From 3df85f24e2d715b2af2daea0ea292d1aae1fca1a Mon Sep 17 00:00:00 2001 From: Alex Reinking Date: Tue, 27 Jan 2026 08:53:50 -0500 Subject: [PATCH 01/67] Initial prototype of neotrace viewer Co-authored-by: Claude Code --- apps/neotrace/README.md | 185 ++++ apps/neotrace/neotrace/__init__.py | 3 + apps/neotrace/neotrace/__main__.py | 101 ++ apps/neotrace/neotrace/viewer.py | 979 ++++++++++++++++++ apps/neotrace/profile_load.py | 52 + apps/neotrace/pyproject.toml | 41 + apps/neotrace/tests/__init__.py | 0 apps/neotrace/tests/test_trace.py | 166 +++ apps/neotrace/uv.lock | 627 +++++++++++ python_bindings/src/halide/CMakeLists.txt | 1 + .../src/halide/halide_/PyHalide.cpp | 2 + .../src/halide/halide_/PyTrace.cpp | 548 ++++++++++ python_bindings/src/halide/halide_/PyTrace.h | 14 + 13 files changed, 2719 insertions(+) create mode 100644 apps/neotrace/README.md create mode 100644 apps/neotrace/neotrace/__init__.py create mode 100644 apps/neotrace/neotrace/__main__.py create mode 100644 apps/neotrace/neotrace/viewer.py create mode 100644 apps/neotrace/profile_load.py create mode 100644 apps/neotrace/pyproject.toml create mode 100644 apps/neotrace/tests/__init__.py create mode 100644 apps/neotrace/tests/test_trace.py create mode 100644 apps/neotrace/uv.lock create mode 100644 python_bindings/src/halide/halide_/PyTrace.cpp create mode 100644 python_bindings/src/halide/halide_/PyTrace.h diff --git a/apps/neotrace/README.md b/apps/neotrace/README.md new file mode 100644 index 000000000000..af3ae63eb15d --- /dev/null +++ b/apps/neotrace/README.md @@ -0,0 +1,185 @@ +# neotrace + +Interactive trace visualization for Halide. + +## Installation + +```bash +cd apps/neotrace +pip install -e . +``` + +For video export support: + +```bash +pip install -e ".[video]" +``` + +## Usage + +### Interactive Viewer + +```bash +# Open the viewer +neotrace view + +# Open with a trace file +neotrace view path/to/trace.bin +``` + +### Trace Info + +```bash +# Print summary of a trace file +neotrace info path/to/trace.bin + +# Verbose output +neotrace info -v path/to/trace.bin +``` + +### Generating Traces + +To generate a trace file from a Halide pipeline: + +```bash +HL_TRACE_FILE=trace.bin HL_TARGET=host-trace_stores ./your_pipeline +``` + +Or trace all operations: + +```bash +HL_TRACE_FILE=trace.bin HL_TARGET=host-trace_all ./your_pipeline +``` + +## Controls + +- **Pan**: Click and drag, or use scroll bars +- **Zoom**: Mouse wheel +- **Select Func**: Click on a Func visualization +- **Move Func**: Drag a selected Func to reposition +- **Timeline**: Use the slider to scrub through the trace +- **Export Config**: File → Export Config to save your layout + +## Development + +```bash +pip install -e ".[dev]" +ruff check . +pytest +``` + +--- + +## Visualization Specification + +### Data Dimensionality & Rendering Modes + +Halide Funcs can have varying dimensionality. Neotrace supports multiple rendering modes: + +| Dimensions | Examples | Default Rendering | +|---------------|---------------------------|---------------------------| +| 0D | Scalar reduction | Single value display | +| 1D | Histogram, LUT | Line or wrapped rectangle | +| 2D | Grayscale image | Heatmap / grayscale | +| 2D + channels | RGB/RGBA image | Color image | +| 3D | Volume, video frame stack | Tiled 2D slices | +| 4D+ | Batch of volumes | Nested tiling | + +#### Rendering Modes + +1. **Grayscale / Heatmap Mode** (1D, 2D) + - Maps scalar values to color via configurable colormap + - Colormaps: `grayscale`, `viridis`, `plasma`, `hot`, `cool` + - Value range: configurable `[min_value, max_value]` + +2. **RGB Mode** (2D + channel dimension) + - Interprets one dimension as color channels (R, G, B, optionally A) + - Channel dimension detected heuristically (dimension with extent 3 or 4) + - Value ranges: `uint8` 0-255, `float32` 0.0-1.0 (configurable) + +3. **Line Mode** (1D) + - Renders 1D data as a horizontal or vertical line + - Height/width configurable (default: 16px) + +4. **Wrapped Mode** (1D) + - Wraps 1D data into a 2D rectangle + - Wrap width auto-computed to approximate square + +5. **Projected Mode** (3D+) + - Reduces higher dimensions by fixing indices + - User specifies which indices to hold constant + - Example: 4D `[batch, y, x, c]` with `batch=0` → RGB image + +6. **Tiled Mode** (3D+) + - Arranges slices in a grid + - User specifies base visualization dims and tiling dims + - Example: `[z, y, x]` → z slices arranged in grid + +### Load & Store Visualization + +- **Stores**: Solid color based on value (current behavior) +- **Loads**: Configurable visual treatment: + - `outline`: Border around accessed pixels + - `heatmap`: Overlay showing access frequency + - `flash`: Brief highlight animation during playback + +### Liveness Visualization + +Funcs are visualized based on their liveness state: + +| State | Condition | Visual Treatment | +|------------|-----------------------------------|--------------------------| +| **Unborn** | Current time < first store | Grayed out (20% opacity) | +| **Alive** | Between first store and last load | Full opacity | +| **Dead** | Current time > last load | Faded (40% opacity) | + +### Axis Indicators + +Each Func displays coordinate range labels: + +- **X-axis**: `[min_x, max_x)` below the image +- **Y-axis**: `[min_y, max_y)` to the left (rotated) + +### Memory Locality View (Future) + +Specialized view for cache/memory access pattern analysis: + +- **Address Heatmap**: Linearized memory with access frequency coloring +- **Access Timeline**: Time vs memory offset showing stride patterns +- **Liveness Bands**: Per-region liveness ranges + +### Configuration Schema + +```yaml +funcs: + "pipeline:func_name": + position: [ x, y ] + zoom: 4.0 + visible: true + + # Value mapping + min_value: 0.0 + max_value: 255.0 + colormap: "grayscale" + + # Dimensionality + render_mode: "auto" # auto, grayscale, rgb, line, wrapped, projected, tiled + channel_dim: -1 # -1 = auto-detect, or explicit dimension index + + # For projected mode + fixed_indices: { } # e.g., {0: 5} to fix dim 0 at index 5 + + # For tiled mode + tile_dims: [ ] # which dims to tile + tile_layout: "auto" # auto, or [rows, cols] + + # For 1D wrapped mode + wrap_width: "auto" + + # Load visualization + show_loads: true + load_style: "outline" # outline, heatmap, flash + + # Liveness + liveness_mode: "fade" # fade, hide, none +``` diff --git a/apps/neotrace/neotrace/__init__.py b/apps/neotrace/neotrace/__init__.py new file mode 100644 index 000000000000..fe734a889e81 --- /dev/null +++ b/apps/neotrace/neotrace/__init__.py @@ -0,0 +1,3 @@ +"""neotrace: Interactive trace visualization for Halide.""" + +__version__ = "0.1.0" diff --git a/apps/neotrace/neotrace/__main__.py b/apps/neotrace/neotrace/__main__.py new file mode 100644 index 000000000000..caad959ca8c3 --- /dev/null +++ b/apps/neotrace/neotrace/__main__.py @@ -0,0 +1,101 @@ +""" +Command-line entry point for neotrace. +""" + +from __future__ import annotations + +import argparse + +import sys +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser( + prog="neotrace", + description="Interactive trace visualization for Halide", + ) + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # view command - interactive viewer + view_parser = subparsers.add_parser("view", help="Open interactive trace viewer") + view_parser.add_argument("trace", type=Path, nargs="?", help="Trace file to open") + view_parser.add_argument("--config", type=Path, help="Layout configuration file") + + # info command - print trace info + info_parser = subparsers.add_parser("info", help="Print trace information") + info_parser.add_argument("trace", type=Path, help="Trace file to analyze") + info_parser.add_argument( + "--verbose", "-v", action="store_true", help="Show detailed info" + ) + info_parser.add_argument( + "--dag", action="store_true", help="Print inferred DAG in DOT format" + ) + + # render command - render to video (future) + render_parser = subparsers.add_parser("render", help="Render trace to video") + render_parser.add_argument("trace", type=Path, help="Trace file to render") + render_parser.add_argument( + "-o", "--output", type=Path, required=True, help="Output video file" + ) + render_parser.add_argument("--config", type=Path, help="Layout configuration file") + render_parser.add_argument( + "--size", nargs=2, type=int, default=[1920, 1080], help="Output size" + ) + render_parser.add_argument("--fps", type=int, default=30, help="Frames per second") + + args = parser.parse_args() + + if args.command is None: + # Default to view if no command specified + args.command = "view" + args.trace = None + args.config = None + + if args.command == "view": + from .viewer import run_viewer + + sys.exit(run_viewer(args.trace)) + + elif args.command == "info": + from tqdm import tqdm + + from halide import Trace + + last_bytes = 0 + with tqdm(unit="B", unit_scale=True, unit_divisor=1024) as pbar: + pbar.set_description("Loading trace") + + def update_progress(bytes_read, total_bytes): + nonlocal last_bytes + pbar.total = total_bytes + pbar.update(bytes_read - last_bytes) + last_bytes = bytes_read + + trace = Trace.load(str(args.trace), update_progress) + + if args.dag: + print(trace.dag_as_dot()) + else: + print(f"Trace: {len(trace)} packets, {len(trace.funcs)} funcs") + print(f"Pipelines: {len(trace.pipelines)}") + if args.verbose: + for name, stats in sorted(trace.funcs.items()): + coords = "" + if stats.min_coords and stats.max_coords: + extents = [ + f"[{lo}, {hi})" + for lo, hi in zip(stats.min_coords, stats.max_coords) + ] + coords = " x ".join(extents) + print(f" {name}: {coords}") + + elif args.command == "render": + print("Render command not yet implemented.") + print("For now, use the interactive viewer to set up your layout,") + print("export the config, and stay tuned for video rendering.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/apps/neotrace/neotrace/viewer.py b/apps/neotrace/neotrace/viewer.py new file mode 100644 index 000000000000..cac5a3b01da2 --- /dev/null +++ b/apps/neotrace/neotrace/viewer.py @@ -0,0 +1,979 @@ +""" +Interactive trace viewer using Qt. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +import graphviz +import numpy as np +from PySide6.QtCore import Qt, QTimer, Signal +from PySide6.QtGui import ( + QBrush, + QColor, + QImage, + QKeySequence, + QPainter, + QPixmap, + QShortcut, + QWheelEvent, +) +from PySide6.QtWidgets import ( + QApplication, + QFileDialog, + QGraphicsItem, + QGraphicsPixmapItem, + QGraphicsScene, + QGraphicsSimpleTextItem, + QGraphicsView, + QHBoxLayout, + QLabel, + QListWidget, + QListWidgetItem, + QMainWindow, + QProgressDialog, + QPushButton, + QSlider, + QSplitter, + QStatusBar, + QVBoxLayout, + QWidget, +) + +from halide import FuncStats, Trace, TracePacket + + +@dataclass +class FuncConfig: + """Configuration for how a Func is displayed.""" + + position: tuple[float, float] = (0.0, 0.0) + zoom: float = 4.0 + min_value: float = 0.0 + max_value: float = 255.0 + color_dim: int = -1 # -1 = grayscale, otherwise index of color dimension + visible: bool = True + + +@dataclass +class ViewerState: + """State of the trace viewer.""" + + trace: Trace | None = None + func_configs: dict[str, FuncConfig] = field(default_factory=dict) + current_time: int = -1 # Index into packets, -1 means nothing rendered yet + playing: bool = False + + +class FuncItem(QGraphicsPixmapItem): + """A draggable graphics item representing a Func's data.""" + + def __init__( + self, + func_name: str, + stats: FuncStats, + config: FuncConfig, + parent: QGraphicsItem | None = None, + ): + super().__init__(parent) + self.func_name = func_name + self.stats = stats + self.config = config + self.label = None # Will be set by TraceCanvas.add_func + self._dirty = False # Track whether pixmap needs regeneration + + # Make it draggable + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True) + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True) + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges, True) + + # Initialize the data array + self._init_data_array() + self._update_pixmap() + + # Create axis labels as child items (move with the func) + self._create_axis_labels() + + def _init_data_array(self): + """Initialize the numpy array for storing Func values.""" + if len(self.stats.min_coords) >= 2: + width = self.stats.max_coords[0] - self.stats.min_coords[0] + height = self.stats.max_coords[1] - self.stats.min_coords[1] + elif len(self.stats.min_coords) == 1: + width = self.stats.max_coords[0] - self.stats.min_coords[0] + height = 1 + else: + width = height = 1 + + # Clamp to reasonable sizes + width = max(1, min(width, 4096)) + height = max(1, min(height, 4096)) + + # RGBA array + self.data = np.zeros((height, width, 4), dtype=np.uint8) + self.data[:, :, 3] = 255 # Fully opaque + self.width = width + self.height = height + self.min_x = self.stats.min_coords[0] if self.stats.min_coords else 0 + self.min_y = self.stats.min_coords[1] if len(self.stats.min_coords) > 1 else 0 + self._dirty = True # Mark for pixmap regeneration + + def _create_axis_labels(self): + """Create axis coordinate labels as child items.""" + label_color = QBrush(QColor(150, 150, 150)) + rendered_w, rendered_h = self.get_rendered_size() + + # X-axis label: [min_x, max_x) below the image + if self.stats.min_coords: + min_x = self.stats.min_coords[0] + max_x = self.stats.max_coords[0] if self.stats.max_coords else min_x + 1 + x_label_text = f"[{min_x}, {max_x})" + self.x_axis_label = QGraphicsSimpleTextItem(x_label_text, self) + self.x_axis_label.setBrush(label_color) + # Center below the image + label_width = self.x_axis_label.boundingRect().width() + self.x_axis_label.setPos((rendered_w - label_width) / 2, rendered_h + 2) + else: + self.x_axis_label = None + + # Y-axis label: [min_y, max_y) to the left, rotated 90 degrees + if len(self.stats.min_coords) > 1: + min_y = self.stats.min_coords[1] + max_y = ( + self.stats.max_coords[1] + if len(self.stats.max_coords) > 1 + else min_y + 1 + ) + y_label_text = f"[{min_y}, {max_y})" + self.y_axis_label = QGraphicsSimpleTextItem(y_label_text, self) + self.y_axis_label.setBrush(label_color) + self.y_axis_label.setRotation(-90) + # Position to the left of the image, centered vertically + label_height = ( + self.y_axis_label.boundingRect().width() + ) # Width becomes height after rotation + self.y_axis_label.setPos(-4, (rendered_h + label_height) / 2) + else: + self.y_axis_label = None + + def get_rendered_size(self) -> tuple[int, int]: + """Calculate the actual rendered size based on zoom.""" + zoom = self.config.zoom + h, w = self.data.shape[:2] + + if zoom >= 1: + izoom = max(1, int(zoom)) + return w * izoom, h * izoom + else: + step = max(1, int(1 / zoom)) + return max(1, w // step), max(1, h // step) + + def _update_pixmap(self): + """Update the pixmap from the data array.""" + zoom = self.config.zoom + h, w = self.data.shape[:2] + + if zoom >= 1: + # Scale up using repeat + izoom = max(1, int(zoom)) + scaled = np.repeat(np.repeat(self.data, izoom, axis=0), izoom, axis=1) + else: + # Scale down using slicing (simple nearest-neighbor downscale) + step = max(1, int(1 / zoom)) + scaled = self.data[::step, ::step, :] + + # Convert to QImage + height, width = scaled.shape[:2] + bytes_per_line = width * 4 + image = QImage( + scaled.tobytes(), + width, + height, + bytes_per_line, + QImage.Format.Format_RGBA8888, + ) + self.setPixmap(QPixmap.fromImage(image)) + + def update_pixel(self, x: int, y: int, value: float, is_store: bool): + """Update a single pixel value.""" + # Normalize coordinates + px = x - self.min_x + py = y - self.min_y + + if 0 <= px < self.width and 0 <= py < self.height: + # Normalize value to 0-255 + min_v = self.config.min_value + max_v = self.config.max_value + if max_v > min_v: + normalized = int(255 * (value - min_v) / (max_v - min_v)) + normalized = max(0, min(255, normalized)) + else: + normalized = 128 + + # Grayscale for now + self.data[py, px, 0] = normalized + self.data[py, px, 1] = normalized + self.data[py, px, 2] = normalized + self._dirty = True + + def refresh_pixmap(self): + """Refresh the pixmap after batch updates, only if data changed.""" + if self._dirty: + self._update_pixmap() + self._dirty = False + + def itemChange(self, change, value): + """Track position changes for config export.""" + if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged: + pos = value + self.config.position = (pos.x(), pos.y()) + return super().itemChange(change, value) + + +class TraceCanvas(QGraphicsView): + """The main canvas for visualizing traces.""" + + func_selected = Signal(str) # Emitted when a func is clicked + + def __init__(self, parent: QWidget | None = None): + super().__init__(parent) + self.scene = QGraphicsScene(self) + self.setScene(self.scene) + + # Enable scrollbars and smooth scrolling + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.setRenderHint(QPainter.RenderHint.Antialiasing) + self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) + + # Background + self.setBackgroundBrush(QBrush(QColor(30, 30, 30))) + + # Track func items and their labels + self.func_items: dict[str, FuncItem] = {} + self.func_labels: dict[str, QGraphicsItem] = {} + + # Zoom state + self._zoom = 1.0 + self._min_zoom = 0.01 + self._max_zoom = 100.0 + + def wheelEvent(self, event: QWheelEvent): + """Handle wheel events: Ctrl/Cmd+scroll zooms, regular scroll pans.""" + # Check for Ctrl (or Cmd on macOS) modifier for zoom + if event.modifiers() & Qt.KeyboardModifier.ControlModifier: + delta = event.angleDelta().y() + if delta != 0: + factor = 1.1 if delta > 0 else 1 / 1.1 + new_zoom = self._zoom * factor + if self._min_zoom <= new_zoom <= self._max_zoom: + self._zoom = new_zoom + # Zoom toward mouse position + self.setTransformationAnchor( + QGraphicsView.ViewportAnchor.AnchorUnderMouse + ) + self.scale(factor, factor) + self.setTransformationAnchor( + QGraphicsView.ViewportAnchor.AnchorViewCenter + ) + event.accept() + else: + # Regular scroll - let parent handle panning + super().wheelEvent(event) + + def zoom_in(self): + """Zoom in by a fixed factor.""" + factor = 1.25 + new_zoom = self._zoom * factor + if new_zoom <= self._max_zoom: + self._zoom = new_zoom + self.scale(factor, factor) + + def zoom_out(self): + """Zoom out by a fixed factor.""" + factor = 1 / 1.25 + new_zoom = self._zoom * factor + if new_zoom >= self._min_zoom: + self._zoom = new_zoom + self.scale(factor, factor) + + def clear_funcs(self): + """Remove all func items and labels.""" + for item in self.func_items.values(): + self.scene.removeItem(item) + for label in self.func_labels.values(): + self.scene.removeItem(label) + self.func_items.clear() + self.func_labels.clear() + + def add_func( + self, func_name: str, stats: FuncStats, config: FuncConfig + ) -> FuncItem: + """Add a Func visualization to the canvas.""" + item = FuncItem(func_name, stats, config) + item.setPos(config.position[0], config.position[1]) + self.scene.addItem(item) + self.func_items[func_name] = item + + # Add label as a child of the item so it moves with it + display_name = func_name.split(":")[-1] if ":" in func_name else func_name + label = self.scene.addSimpleText(display_name) + label.setParentItem(item) # Make label a child of the func item + label.setPos(0, -20) # Position relative to parent (above it) + label.setBrush(QBrush(QColor(200, 200, 200))) + item.label = label # Store reference on item + self.func_labels[func_name] = label + + return item + + def get_func_item(self, func_name: str) -> FuncItem | None: + """Get a func item by name.""" + return self.func_items.get(func_name) + + +class TimelineWidget(QWidget): + """Timeline scrubber for navigating the trace.""" + + time_changed = Signal(int) + play_toggled = Signal(bool) # True = playing, False = paused + + def __init__(self, parent: QWidget | None = None): + super().__init__(parent) + layout = QHBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + + self.play_button = QPushButton("Play") + self.play_button.setCheckable(True) + self.play_button.clicked.connect(self._on_play_clicked) + layout.addWidget(self.play_button) + + self.slider = QSlider(Qt.Orientation.Horizontal) + self.slider.setMinimum(0) + self.slider.setMaximum(0) + self.slider.valueChanged.connect(self._on_slider_changed) + layout.addWidget(self.slider, stretch=1) + + self.time_label = QLabel("0 / 0") + self.time_label.setMinimumWidth(100) + layout.addWidget(self.time_label) + + def set_range(self, max_time: int): + """Set the maximum time value.""" + self.slider.setMaximum(max_time) + self._update_label() + + def set_time(self, time: int): + """Set the current time without emitting signal.""" + self.slider.blockSignals(True) + self.slider.setValue(time) + self.slider.blockSignals(False) + self._update_label() + + def stop_playback(self): + """Stop playback and reset button state.""" + self.play_button.setChecked(False) + self.play_button.setText("Play") + + def _update_label(self): + self.time_label.setText(f"{self.slider.value()} / {self.slider.maximum()}") + + def _on_slider_changed(self, value: int): + self._update_label() + self.time_changed.emit(value) + + def _on_play_clicked(self, checked: bool): + self.play_button.setText("Pause" if checked else "Play") + self.play_toggled.emit(checked) + + +class FuncListWidget(QWidget): + """Sidebar widget showing list of Funcs.""" + + func_visibility_changed = Signal(str, bool) + + def __init__(self, parent: QWidget | None = None): + super().__init__(parent) + layout = QVBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + + layout.addWidget(QLabel("Funcs")) + + self.list_widget = QListWidget() + self.list_widget.itemChanged.connect(self._on_item_changed) + layout.addWidget(self.list_widget) + + def set_funcs(self, funcs: dict[str, FuncStats]): + """Populate the list with funcs.""" + self.list_widget.clear() + for name, stats in sorted(funcs.items()): + item = QListWidgetItem(name) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState(Qt.CheckState.Checked) + item.setData(Qt.ItemDataRole.UserRole, stats) + self.list_widget.addItem(item) + + def _on_item_changed(self, item: QListWidgetItem): + name = item.text() + visible = item.checkState() == Qt.CheckState.Checked + self.func_visibility_changed.emit(name, visible) + + +class TraceViewer(QMainWindow): + """Main window for the trace viewer.""" + + def __init__(self): + super().__init__() + self.setWindowTitle("neotrace - Halide Trace Viewer") + self.resize(1600, 900) + + self.state = ViewerState() + + # Playback timer + self._playback_timer = QTimer(self) + self._playback_timer.timeout.connect(self._on_playback_tick) + self._playback_step = 10000 # Packets per tick (increased for performance) + + # Cache for func name lookups + self._func_name_cache: dict[str, FuncItem | None] = {} + + self._setup_ui() + self._setup_menu() + + def _setup_ui(self): + """Set up the UI components.""" + central = QWidget() + self.setCentralWidget(central) + main_layout = QVBoxLayout(central) + main_layout.setContentsMargins(0, 0, 0, 0) + + # Splitter for sidebar and canvas + splitter = QSplitter(Qt.Orientation.Horizontal) + main_layout.addWidget(splitter, stretch=1) + + # Sidebar + self.func_list = FuncListWidget() + self.func_list.setMaximumWidth(250) + self.func_list.func_visibility_changed.connect(self._on_func_visibility_changed) + splitter.addWidget(self.func_list) + + # Canvas + self.canvas = TraceCanvas() + splitter.addWidget(self.canvas) + + splitter.setSizes([200, 1400]) + + # Keyboard shortcuts for zoom + zoom_in_shortcut = QShortcut(QKeySequence.StandardKey.ZoomIn, self) + zoom_in_shortcut.activated.connect(self.canvas.zoom_in) + zoom_in_shortcut2 = QShortcut( + QKeySequence("Ctrl+="), self + ) # For keyboards without + + zoom_in_shortcut2.activated.connect(self.canvas.zoom_in) + + zoom_out_shortcut = QShortcut(QKeySequence.StandardKey.ZoomOut, self) + zoom_out_shortcut.activated.connect(self.canvas.zoom_out) + + # Timeline + self.timeline = TimelineWidget() + self.timeline.time_changed.connect(self._on_time_changed) + self.timeline.play_toggled.connect(self._on_play_toggled) + main_layout.addWidget(self.timeline) + + # Status bar + self.status_bar = QStatusBar() + self.setStatusBar(self.status_bar) + + def _setup_menu(self): + """Set up the menu bar.""" + menu_bar = self.menuBar() + + file_menu = menu_bar.addMenu("&File") + file_menu.addAction("&Open...", self._open_file, "Ctrl+O") + file_menu.addSeparator() + file_menu.addAction("&Export Config...", self._export_config, "Ctrl+S") + file_menu.addSeparator() + file_menu.addAction("&Quit", self.close, "Ctrl+Q") + + view_menu = menu_bar.addMenu("&View") + view_menu.addAction("&Reset Zoom", self._reset_zoom, "Ctrl+0") + view_menu.addAction("&Fit All", self._fit_all, "Ctrl+F") + + def _open_file(self): + """Open a trace file.""" + path, _ = QFileDialog.getOpenFileName( + self, + "Open Trace File", + "", + "Trace Files (*.bin);;All Files (*)", + ) + if path: + self.load_trace(Path(path)) + + def load_trace(self, path: Path): + """Load a trace file and display it.""" + # Create progress dialog + progress = QProgressDialog(f"Loading {path.name}...", "Cancel", 0, 100, self) + progress.setWindowTitle("Loading Trace") + progress.setMinimumDuration(500) # Show after 500ms + progress.setAutoClose(True) + progress.setAutoReset(True) + + def on_progress(bytes_read: int, total_bytes: int): + if progress.wasCanceled(): + raise InterruptedError("Loading cancelled") + percent = int(100 * bytes_read / total_bytes) if total_bytes > 0 else 0 + progress.setValue(percent) + QApplication.processEvents() + + try: + trace = Trace.load(str(path), progress_callback=on_progress) + progress.setValue(100) + + self.state.trace = trace + self.state.current_time = -1 # Reset so first render processes all packets + + # Clear func name cache + self._func_name_cache = {} + + # Initialize func configs with auto-layout + self._auto_layout() + + # Update UI + self.func_list.set_funcs(trace.funcs) + self.timeline.set_range(len(trace.packets) - 1) + + # Add func items to canvas + self.canvas.clear_funcs() + for name, stats in trace.funcs.items(): + config = self.state.func_configs.get(name) + if config and config.visible: + self.canvas.add_func(name, stats, config) + + # Don't render initially - let user scrub to desired time + # This avoids the long initial render for large traces + self.timeline.set_time(0) + + self.status_bar.showMessage( + f"Loaded {path.name}: {len(trace.packets)} packets, {len(trace.funcs)} funcs" + ) + except InterruptedError: + self.status_bar.showMessage("Loading cancelled") + except Exception as e: + self.status_bar.showMessage(f"Error loading trace: {e}") + raise + + def _calc_rendered_size( + self, width: int, height: int, zoom: float + ) -> tuple[int, int]: + """Calculate actual rendered size matching FuncItem._update_pixmap logic.""" + if zoom >= 1: + izoom = max(1, int(zoom)) + return width * izoom, height * izoom + else: + step = max(1, int(1 / zoom)) + return max(1, width // step), max(1, height // step) + + def _dag_layout( + self, func_sizes: dict[str, tuple[int, int]] + ) -> dict[str, tuple[float, float]] | None: + """Compute layout positions using GraphViz based on DAG structure. + + Args: + func_sizes: dict mapping func name to (rendered_width, rendered_height) + + Returns dict mapping func name to (x, y) position, or None if layout fails. + """ + if not self.state.trace or not self.state.trace.dag_edges: + return None + + trace = self.state.trace + funcs = set(trace.funcs.keys()) + + # GraphViz uses inches for dimensions (72 points = 1 inch) + # We'll convert pixel sizes to inches for proper spacing + DPI = 72.0 + + try: + dot = graphviz.Digraph(engine="dot") + dot.attr(rankdir="LR") # Left to right (inputs on left, outputs on right) + dot.attr("node", shape="box") + # Increase separation between nodes and ranks for clarity + dot.attr("graph", nodesep="0.5", ranksep="1.0") + + # Use sanitized IDs to avoid GraphViz port syntax issues with colons + # Map: sanitized_id -> original_name + id_to_name: dict[str, str] = {} + for name in funcs: + # Replace colons with underscores for GraphViz node IDs + node_id = name.replace(":", "_") + id_to_name[node_id] = name + short_name = name.split(":")[-1] if ":" in name else name + + # Tell GraphViz the actual size of each node so it can space properly + if name in func_sizes: + pw, ph = func_sizes[name] + # Convert pixels to inches, add padding for labels + width_in = (pw + 20) / DPI + height_in = (ph + 30) / DPI # Extra for label above + else: + width_in = height_in = 1.0 + + dot.node( + node_id, + short_name, + width=f"{width_in:.2f}", + height=f"{height_in:.2f}", + fixedsize="true", + ) + + for producer, consumers in trace.dag_edges.items(): + if producer in funcs: + producer_id = producer.replace(":", "_") + for consumer in consumers: + if consumer in funcs: + consumer_id = consumer.replace(":", "_") + dot.edge(producer_id, consumer_id) + + plain = dot.pipe(format="plain").decode("utf-8") + + positions: dict[str, tuple[float, float]] = {} + + for line in plain.split("\n"): + parts = line.split() + if len(parts) >= 5 and parts[0] == "node": + node_id = parts[1] + # GraphViz plain format: x and y are in inches, convert to pixels + x = float(parts[2]) * DPI + y = float(parts[3]) * DPI + # Map back to original name + if node_id in id_to_name: + positions[id_to_name[node_id]] = (x, y) + + return positions if positions else None + except Exception as e: + self.status_bar.showMessage(f"GraphViz layout failed: {e}") + return None + + def _auto_layout(self): + """Automatically lay out funcs using DAG structure when available.""" + if not self.state.trace: + return + + funcs = list(self.state.trace.funcs.items()) + n = len(funcs) + if n == 0: + return + + # Target cell size for layout + target_cell_size = 250 + padding = 30 + label_height = 25 + + # First pass: calculate zoom and rendered size for each func + # Maps name -> (zoom, rendered_width, rendered_height, min_val, max_val) + func_info: dict[str, tuple[float, int, int, float, float]] = {} + func_sizes: dict[str, tuple[int, int]] = {} # For GraphViz layout + for name, stats in funcs: + if stats.min_coords and stats.max_coords: + width = max(1, stats.max_coords[0] - stats.min_coords[0]) + height = max( + 1, + stats.max_coords[1] - stats.min_coords[1] + if len(stats.max_coords) > 1 + else 1, + ) + # Calculate zoom to fit in target cell (allow zoom < 1 for large funcs) + available = target_cell_size - padding + zoom_x = available / width + zoom_y = available / height + zoom = min(zoom_x, zoom_y) + # Clamp zoom to reasonable range + zoom = max(0.1, min(zoom, 8)) + # Calculate actual rendered size using same logic as FuncItem + rendered_width, rendered_height = self._calc_rendered_size( + width, height, zoom + ) + else: + zoom = 4 + rendered_width, rendered_height = self._calc_rendered_size(1, 1, zoom) + + # Determine value range + min_val = stats.min_value if stats.min_value is not None else 0.0 + max_val = stats.max_value if stats.max_value is not None else 255.0 + if min_val == max_val: + max_val = min_val + 1 + + func_info[name] = (zoom, rendered_width, rendered_height, min_val, max_val) + func_sizes[name] = (rendered_width, rendered_height) + + # Try DAG-based layout first + dag_positions = self._dag_layout(func_sizes) + + if dag_positions: + # Use DAG positions directly - GraphViz already accounts for node sizes + # Positions are node centers, so we offset to get top-left corner + for name in func_info: + zoom, rw, rh, min_val, max_val = func_info[name] + if name in dag_positions: + cx, cy = dag_positions[name] + # Convert from center to top-left, add padding + px = cx - rw / 2 + padding + py = cy - rh / 2 + padding + label_height + else: + # Func not in DAG, place at the end + px = padding + py = padding + label_height + + config = FuncConfig( + position=(px, py), + zoom=zoom, + min_value=min_val, + max_value=max_val, + ) + self.state.func_configs[name] = config + else: + # Fallback to grid layout + self._grid_layout(funcs, func_info, padding, label_height) + + def _grid_layout( + self, + funcs: list[tuple[str, FuncStats]], + func_info: dict[str, tuple[float, int, int, float, float]], + padding: int, + label_height: int, + ): + """Lay out funcs in a simple grid.""" + n = len(funcs) + cols = int(np.ceil(np.sqrt(n))) + + current_y = padding + col = 0 + row_height = 0 + row_items: list[tuple[str, float, int, int, float, float]] = [] + + for name, _stats in funcs: + zoom, rw, rh, min_val, max_val = func_info[name] + cell_height = rh + padding + label_height + + # Start new row if needed + if col >= cols: + # Position items in the completed row + current_x = padding + for ( + item_name, + item_zoom, + item_rw, + _item_rh, + item_min, + item_max, + ) in row_items: + config = FuncConfig( + position=(current_x, current_y + label_height), + zoom=item_zoom, + min_value=item_min, + max_value=item_max, + ) + self.state.func_configs[item_name] = config + current_x += item_rw + padding + + current_y += row_height + padding + row_items = [] + row_height = 0 + col = 0 + + row_items.append((name, zoom, rw, rh, min_val, max_val)) + row_height = max(row_height, cell_height) + col += 1 + + # Position remaining items in last row + current_x = padding + for item_name, item_zoom, item_rw, _item_rh, item_min, item_max in row_items: + config = FuncConfig( + position=(current_x, current_y + label_height), + zoom=item_zoom, + min_value=item_min, + max_value=item_max, + ) + self.state.func_configs[item_name] = config + current_x += item_rw + padding + + def _render_to_time(self, time: int): + """Render the trace state at the given time index.""" + if not self.state.trace: + return + + last_time = self.state.current_time + + # Determine rendering strategy + if time > last_time and last_time >= 0: + # Moving forward: incremental update + self._render_range(last_time + 1, time + 1) + elif time < last_time: + # Moving backward: must re-render from scratch + for item in self.canvas.func_items.values(): + item._init_data_array() + self._render_range(0, time + 1) + # else: time == last_time, nothing to do + + # Refresh all pixmaps + for item in self.canvas.func_items.values(): + item.refresh_pixmap() + + self.state.current_time = time + + def _render_range(self, start: int, end: int): + """Process packets in the given range [start, end).""" + if not self.state.trace: + return + + packets = self.state.trace.packets + for i in range(start, min(end, len(packets))): + packet = packets[i] + if packet.is_store: + self._process_store(packet) + + def _process_store(self, packet: TracePacket): + """Process a store packet.""" + # Use cached lookup if available + func_name = packet.func + item = self._get_func_item_for_packet(func_name) + if item is None: + return + + values = packet.get_values() + if not values: + return + + dims_per_lane = ( + packet.dimensions // packet.type.lanes + if packet.type.lanes > 0 + else packet.dimensions + ) + + for lane in range(packet.type.lanes): + # Get coordinates for this lane + if dims_per_lane >= 2: + x = packet.coordinates[0 * packet.type.lanes + lane] + y = packet.coordinates[1 * packet.type.lanes + lane] + elif dims_per_lane == 1: + x = packet.coordinates[lane] + y = 0 + else: + x = y = 0 + + if lane < len(values): + item.update_pixel(x, y, values[lane], is_store=True) + + def _get_func_item_for_packet(self, func_name: str) -> FuncItem | None: + """Get the FuncItem for a packet's func name, with caching.""" + if func_name in self._func_name_cache: + return self._func_name_cache[func_name] + + # Search for matching item + for name, item in self.canvas.func_items.items(): + if func_name in name or name.endswith(f":{func_name}"): + self._func_name_cache[func_name] = item + return item + + # Not found + self._func_name_cache[func_name] = None + return None + + def _on_time_changed(self, time: int): + """Handle timeline scrubbing.""" + self._render_to_time(time) + + def _on_play_toggled(self, playing: bool): + """Handle play/pause toggle.""" + if playing: + # Start playback - use 30ms interval for ~33 fps + self._playback_timer.start(30) + self.state.playing = True + else: + self._playback_timer.stop() + self.state.playing = False + + def _on_playback_tick(self): + """Advance playback by one step.""" + if not self.state.trace: + return + + max_time = len(self.state.trace.packets) - 1 + new_time = min(self.state.current_time + self._playback_step, max_time) + + if new_time >= max_time: + # Reached the end - stop playback + self._playback_timer.stop() + self.state.playing = False + self.timeline.stop_playback() + + self.timeline.set_time(new_time) + self._render_to_time(new_time) + + def _on_func_visibility_changed(self, func_name: str, visible: bool): + """Handle func visibility toggle.""" + if func_name in self.state.func_configs: + self.state.func_configs[func_name].visible = visible + # Show/hide the item and its label + if func_name in self.canvas.func_items: + self.canvas.func_items[func_name].setVisible(visible) + if func_name in self.canvas.func_labels: + self.canvas.func_labels[func_name].setVisible(visible) + + def _export_config(self): + """Export the current layout configuration.""" + path, _ = QFileDialog.getSaveFileName( + self, + "Export Configuration", + "layout.yaml", + "YAML Files (*.yaml *.yml);;All Files (*)", + ) + if path: + self._save_config(Path(path)) + + def _save_config(self, path: Path): + """Save configuration to a YAML file.""" + import yaml + + config = { + "funcs": { + name: { + "position": list(cfg.position), + "zoom": cfg.zoom, + "min_value": cfg.min_value, + "max_value": cfg.max_value, + "color_dim": cfg.color_dim, + "visible": cfg.visible, + } + for name, cfg in self.state.func_configs.items() + } + } + with path.open("w") as f: + yaml.dump(config, f, default_flow_style=False) + self.status_bar.showMessage(f"Saved config to {path}") + + def _reset_zoom(self): + """Reset canvas zoom to 100%.""" + self.canvas.resetTransform() + self.canvas._zoom = 1.0 + + def _fit_all(self): + """Fit all items in view.""" + bounds = self.canvas.scene.itemsBoundingRect() + self.canvas.fitInView(bounds, Qt.AspectRatioMode.KeepAspectRatio) + + +def run_viewer(trace_path: Path | None = None): + """Run the interactive viewer.""" + app = QApplication.instance() or QApplication([]) + + viewer = TraceViewer() + viewer.show() + + if trace_path: + viewer.load_trace(trace_path) + + return app.exec() diff --git a/apps/neotrace/profile_load.py b/apps/neotrace/profile_load.py new file mode 100644 index 000000000000..a62f85375c40 --- /dev/null +++ b/apps/neotrace/profile_load.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Profile trace loading to identify bottlenecks.""" + +import cProfile +import pstats +import sys +from pathlib import Path + + +def main(): + if len(sys.argv) < 2: + print("Usage: python profile_load.py ") + sys.exit(1) + + trace_path = Path(sys.argv[1]) + if not trace_path.exists(): + print(f"File not found: {trace_path}") + sys.exit(1) + + print(f"Profiling load of {trace_path} ({trace_path.stat().st_size / 1024 / 1024:.1f} MB)") + print() + + from neotrace.trace import Trace + + # Profile the load + profiler = cProfile.Profile() + profiler.enable() + + trace = Trace.load(str(trace_path)) + + profiler.disable() + + print(f"Loaded {len(trace)} packets, {len(trace.funcs)} funcs") + print() + + # Show stats sorted by cumulative time + stats = pstats.Stats(profiler) + stats.strip_dirs() + + print("=" * 70) + print("Top 30 functions by cumulative time:") + print("=" * 70) + stats.sort_stats("cumulative").print_stats(30) + + print("=" * 70) + print("Top 30 functions by total time (self, excluding subcalls):") + print("=" * 70) + stats.sort_stats("tottime").print_stats(30) + + +if __name__ == "__main__": + main() diff --git a/apps/neotrace/pyproject.toml b/apps/neotrace/pyproject.toml new file mode 100644 index 000000000000..856a63cecac6 --- /dev/null +++ b/apps/neotrace/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "neotrace" +version = "0.1.0" +description = "Interactive trace visualization for Halide" +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" +dependencies = [ + "PySide6>=6.5", + "graphviz>=0.20", + "halide", + "imageio[ffmpeg]>=2.31", + "numpy>=1.24", + "tqdm>=4.67.1", +] + +[project.scripts] +neotrace = "neotrace.__main__:main" + + +[tool.ruff] +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM"] + +[tool.uv.sources] +halide = { path = "../.." } + +[tool.hatch.build.targets.wheel] +packages = ["neotrace"] + +[dependency-groups] +dev = [ + "pytest>=9.0.2", + "ruff>=0.14.13", +] diff --git a/apps/neotrace/tests/__init__.py b/apps/neotrace/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/apps/neotrace/tests/test_trace.py b/apps/neotrace/tests/test_trace.py new file mode 100644 index 000000000000..8228dc4c7852 --- /dev/null +++ b/apps/neotrace/tests/test_trace.py @@ -0,0 +1,166 @@ +"""Tests for the trace reader.""" + +import struct + + +def test_imports(): + """Test that all modules can be imported.""" + from neotrace.trace import EventCode, FuncStats, HalideType, Trace, TypeCode + from neotrace.viewer import TraceViewer + + +def test_native_availability(): + """Report which trace implementation is being used.""" + from neotrace.trace import _NATIVE_TRACE_AVAILABLE + # This test just reports the status, doesn't assert + print(f"Native trace module available: {_NATIVE_TRACE_AVAILABLE}") + + +from neotrace.trace import EventCode, HalideType, Trace, TypeCode, _NATIVE_TRACE_AVAILABLE + + +def make_packet( + packet_id: int, + event: EventCode, + parent_id: int, + func: str, + type_code: TypeCode = TypeCode.FLOAT, + type_bits: int = 32, + type_lanes: int = 1, + coordinates: tuple[int, ...] = (), + values: tuple[float | int, ...] = (), + trace_tag: str = "", + value_index: int = 0, +) -> bytes: + """Create a binary trace packet.""" + # Coordinates + coords_bytes = struct.pack(f"<{len(coordinates)}i", *coordinates) + + # Values + if type_code == TypeCode.FLOAT and type_bits == 32: + values_bytes = struct.pack(f"<{len(values)}f", *values) + elif type_code == TypeCode.FLOAT and type_bits == 64: + values_bytes = struct.pack(f"<{len(values)}d", *values) + elif type_code == TypeCode.INT: + fmt = {8: "b", 16: "h", 32: "i", 64: "q"}[type_bits] + values_bytes = struct.pack(f"<{len(values)}{fmt}", *values) + elif type_code == TypeCode.UINT: + fmt = {8: "B", 16: "H", 32: "I", 64: "Q"}[type_bits] + values_bytes = struct.pack(f"<{len(values)}{fmt}", *values) + else: + values_bytes = b"" + + # Strings + func_bytes = func.encode("utf-8") + b"\x00" + tag_bytes = trace_tag.encode("utf-8") + b"\x00" + + # Header + header_size = 28 + payload_size = ( + len(coords_bytes) + len(values_bytes) + len(func_bytes) + len(tag_bytes) + ) + total_size = header_size + payload_size + + header = struct.pack( + "= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "graphviz" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, +] + +[[package]] +name = "halide" +source = { directory = "../../" } +dependencies = [ + { name = "imageio" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.metadata] +requires-dist = [ + { name = "imageio", specifier = ">=2" }, + { name = "numpy", specifier = ">=1.26" }, +] + +[package.metadata.requires-dev] +apps = [ + { name = "onnx", specifier = ">=1.18.0" }, + { name = "pytest" }, +] +dev = [ + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, +] +tools = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "ninja", specifier = ">=1.11" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "tbump", specifier = ">=6.11" }, +] + +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + +[package.optional-dependencies] +ffmpeg = [ + { name = "imageio-ffmpeg" }, + { name = "psutil" }, +] + +[[package]] +name = "imageio-ffmpeg" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" }, + { url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "neotrace" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "graphviz" }, + { name = "halide" }, + { name = "imageio", extra = ["ffmpeg"] }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyside6" }, + { name = "tqdm" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "graphviz", specifier = ">=0.20" }, + { name = "halide", directory = "../../" }, + { name = "imageio", extras = ["ffmpeg"], specifier = ">=2.31" }, + { name = "numpy", specifier = ">=1.24" }, + { name = "pyside6", specifier = ">=6.5" }, + { name = "tqdm", specifier = ">=4.67.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.2" }, + { name = "ruff", specifier = ">=0.14.13" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, + { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, + { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, + { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, + { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, + { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, + { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, + { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" }, + { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, + { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, + { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, + { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, + { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, + { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, + { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, + { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, + { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, + { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, + { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, + { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, + { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyside6" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-addons" }, + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/22/f82cfcd1158be502c5741fe67c3fa853f3c1edbd3ac2c2250769dd9722d1/pyside6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:d0e70dd0e126d01986f357c2a555722f9462cf8a942bf2ce180baf69f468e516", size = 558169, upload-time = "2025-11-20T10:09:08.79Z" }, + { url = "https://files.pythonhosted.org/packages/66/eb/54afe242a25d1c33b04ecd8321a549d9efb7b89eef7690eed92e98ba1dc9/pyside6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4053bf51ba2c2cb20e1005edd469997976a02cec009f7c46356a0b65c137f1fa", size = 557818, upload-time = "2025-11-20T10:09:10.132Z" }, + { url = "https://files.pythonhosted.org/packages/4d/af/5706b1b33587dc2f3dfa3a5000424befba35e4f2d5889284eebbde37138b/pyside6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7d3ca20a40139ca5324a7864f1d91cdf2ff237e11bd16354a42670f2a4eeb13c", size = 558358, upload-time = "2025-11-20T10:09:11.288Z" }, + { url = "https://files.pythonhosted.org/packages/26/41/3f48d724ecc8e42cea8a8442aa9b5a86d394b85093275990038fd1020039/pyside6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9f89ff994f774420eaa38cec6422fddd5356611d8481774820befd6f3bb84c9e", size = 564424, upload-time = "2025-11-20T10:09:12.677Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/395411473b433875a82f6b5fdd0cb28f19a0e345bcaac9fbc039400d7072/pyside6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:9c5c1d94387d1a32a6fae25348097918ef413b87dfa3767c46f737c6d48ae437", size = 548866, upload-time = "2025-11-20T10:09:14.174Z" }, +] + +[[package]] +name = "pyside6-addons" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/f9/b72a2578d7dbef7741bb90b5756b4ef9c99a5b40148ea53ce7f048573fe9/pyside6_addons-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4d2b82bbf9b861134845803837011e5f9ac7d33661b216805273cf0c6d0f8e82", size = 322639446, upload-time = "2025-11-20T09:54:50.75Z" }, + { url = "https://files.pythonhosted.org/packages/94/3b/3ed951c570a15570706a89d39bfd4eaaffdf16d5c2dca17e82fc3ec8aaa6/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:330c229b58d30083a7b99ed22e118eb4f4126408429816a4044ccd0438ae81b4", size = 170678293, upload-time = "2025-11-20T09:56:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/22/77/4c780b204d0bf3323a75c184e349d063e208db44c993f1214aa4745d6f47/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:56864b5fecd6924187a2d0f7e98d968ed72b6cc267caa5b294cd7e88fff4e54c", size = 166365011, upload-time = "2025-11-20T09:57:20.261Z" }, + { url = "https://files.pythonhosted.org/packages/04/14/58239776499e6b279fa6ca2e0d47209531454b99f6bd2ad7c96f11109416/pyside6_addons-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:b6e249d15407dd33d6a2ffabd9dc6d7a8ab8c95d05f16a71dad4d07781c76341", size = 164864664, upload-time = "2025-11-20T09:57:54.815Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cd/1b74108671ba4b1ebb2661330665c4898b089e9c87f7ba69fe2438f3d1b6/pyside6_addons-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:0de303c0447326cdc6c8be5ab066ef581e2d0baf22560c9362d41b8304fdf2db", size = 34191225, upload-time = "2025-11-20T09:58:04.184Z" }, +] + +[[package]] +name = "pyside6-essentials" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/b0/c43209fecef79912e9b1c70a1b5172b1edf76caebcc885c58c60a09613b0/pyside6_essentials-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:cd224aff3bb26ff1fca32c050e1c4d0bd9f951a96219d40d5f3d0128485b0bbe", size = 105461499, upload-time = "2025-11-20T09:59:23.733Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8e/b69ba7fa0c701f3f4136b50460441697ec49ee6ea35c229eb2a5ee4b5952/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e9ccbfb58c03911a0bce1f2198605b02d4b5ca6276bfc0cbcf7c6f6393ffb856", size = 76764617, upload-time = "2025-11-20T09:59:38.831Z" }, + { url = "https://files.pythonhosted.org/packages/bd/83/569d27f4b6c6b9377150fe1a3745d64d02614021bea233636bc936a23423/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ec8617c9b143b0c19ba1cc5a7e98c538e4143795480cb152aee47802c18dc5d2", size = 75850373, upload-time = "2025-11-20T09:59:56.082Z" }, + { url = "https://files.pythonhosted.org/packages/1e/64/a8df6333de8ccbf3a320e1346ca30d0f314840aff5e3db9b4b66bf38e26c/pyside6_essentials-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9555a48e8f0acf63fc6a23c250808db841b28a66ed6ad89ee0e4df7628752674", size = 74491180, upload-time = "2025-11-20T10:00:11.215Z" }, + { url = "https://files.pythonhosted.org/packages/67/da/65cc6c6a870d4ea908c59b2f0f9e2cf3bfc6c0710ebf278ed72f69865e4e/pyside6_essentials-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:4d1d248644f1778f8ddae5da714ca0f5a150a5e6f602af2765a7d21b876da05c", size = 55190458, upload-time = "2025-11-20T10:00:26.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +] + +[[package]] +name = "shiboken6" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/8b/e5db743d505ceea3efc4cd9634a3bee22a3e2bf6e07cefd28c9b9edabcc6/shiboken6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:9f2990f5b61b0b68ecadcd896ab4441f2cb097eef7797ecc40584107d9850d71", size = 478483, upload-time = "2025-11-20T10:08:52.411Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/b50c1a44b3c4643f482afbf1a0ea58f393827307100389ce29404f9ad3b0/shiboken6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4221a52dfb81f24a0d20cc4f8981cb6edd810d5a9fb28287ce10d342573a0e4", size = 271993, upload-time = "2025-11-20T10:08:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/16/b8/939c24ebd662b0aa5c945443d0973145b3fb7079f0196274ef7bb4b98f73/shiboken6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c095b00f4d6bf578c0b2464bb4e264b351a99345374478570f69e2e679a2a1d0", size = 268691, upload-time = "2025-11-20T10:08:55.639Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a6/8c65ee0fa5e172ebcca03246b1bc3bd96cdaf1d60537316648536b7072a5/shiboken6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:c1601d3cda1fa32779b141663873741b54e797cb0328458d7466281f117b0a4e", size = 1234704, upload-time = "2025-11-20T10:08:57.417Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6a/c0fea2f2ac7d9d96618c98156500683a4d1f93fea0e8c5a2bc39913d7ef1/shiboken6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:5cf800917008587b551005a45add2d485cca66f5f7ecd5b320e9954e40448cc9", size = 1795567, upload-time = "2025-11-20T10:08:59.184Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/python_bindings/src/halide/CMakeLists.txt b/python_bindings/src/halide/CMakeLists.txt index 138625bbf149..f572722cde66 100644 --- a/python_bindings/src/halide/CMakeLists.txt +++ b/python_bindings/src/halide/CMakeLists.txt @@ -33,6 +33,7 @@ target_sources( halide_/PySerialization.cpp halide_/PyStage.cpp halide_/PyTarget.cpp + halide_/PyTrace.cpp halide_/PyTuple.cpp halide_/PyType.cpp halide_/PyVar.cpp diff --git a/python_bindings/src/halide/halide_/PyHalide.cpp b/python_bindings/src/halide/halide_/PyHalide.cpp index dc2755bf6994..3a6c0dbea802 100644 --- a/python_bindings/src/halide/halide_/PyHalide.cpp +++ b/python_bindings/src/halide/halide_/PyHalide.cpp @@ -23,6 +23,7 @@ #include "PyRDom.h" #include "PySerialization.h" #include "PyTarget.h" +#include "PyTrace.h" #include "PyTuple.h" #include "PyType.h" #include "PyVar.h" @@ -77,6 +78,7 @@ PYBIND11_MODULE(HALIDE_PYBIND_MODULE_NAME, m) { define_derivative(m); define_generator(m); define_serialization(m); + define_trace(m); // There is no PyUtil yet, so just put this here m.def("load_plugin", &Halide::load_plugin, py::arg("lib_name")); diff --git a/python_bindings/src/halide/halide_/PyTrace.cpp b/python_bindings/src/halide/halide_/PyTrace.cpp new file mode 100644 index 000000000000..04165d435553 --- /dev/null +++ b/python_bindings/src/halide/halide_/PyTrace.cpp @@ -0,0 +1,548 @@ +#include "PyTrace.h" + +#include "HalideRuntime.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Halide::PythonBindings { + +// Statistics about a traced Func +struct FuncStats { + std::string name; + std::vector min_coords; + std::vector max_coords; + std::optional min_value; + std::optional max_value; +}; + +// A single trace packet +struct TracePacket { + int32_t id; + int32_t event; + int32_t parent_id; + int32_t value_index; + uint8_t type_code; + uint8_t type_bits; + uint16_t type_lanes; + std::vector coordinates; + std::vector value; + std::string func; + std::string trace_tag; + + bool is_load() const { + return event == halide_trace_load; + } + bool is_store() const { + return event == halide_trace_store; + } + bool is_load_or_store() const { + return is_load() || is_store(); + } + + py::object get_values() const { + if (value.empty()) { + return py::list(); + } + + py::list result; + const size_t elem_size = (type_bits + 7) / 8; + const size_t count = type_lanes; + + for (size_t i = 0; i < count && (i + 1) * elem_size <= value.size(); ++i) { + const uint8_t *ptr = value.data() + i * elem_size; + if (type_code == halide_type_float) { + if (type_bits == 32) { + float v; + std::memcpy(&v, ptr, sizeof(v)); + result.append(v); + } else if (type_bits == 64) { + double v; + std::memcpy(&v, ptr, sizeof(v)); + result.append(v); + } + } else if (type_code == halide_type_int) { + if (type_bits == 8) { + result.append(static_cast(*ptr)); + } else if (type_bits == 16) { + int16_t v; + std::memcpy(&v, ptr, sizeof(v)); + result.append(v); + } else if (type_bits == 32) { + int32_t v; + std::memcpy(&v, ptr, sizeof(v)); + result.append(v); + } else if (type_bits == 64) { + int64_t v; + std::memcpy(&v, ptr, sizeof(v)); + result.append(v); + } + } else if (type_code == halide_type_uint) { + if (type_bits == 8) { + result.append(*ptr); + } else if (type_bits == 16) { + uint16_t v; + std::memcpy(&v, ptr, sizeof(v)); + result.append(v); + } else if (type_bits == 32) { + uint32_t v; + std::memcpy(&v, ptr, sizeof(v)); + result.append(v); + } else if (type_bits == 64) { + uint64_t v; + std::memcpy(&v, ptr, sizeof(v)); + result.append(v); + } + } + } + return result; + } +}; + +// A complete Halide trace +class Trace { +public: + static Trace load(const std::string &path, + std::optional> progress_callback = std::nullopt) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) { + throw std::runtime_error("Failed to open trace file: " + path); + } + + const size_t total_size = file.tellg(); + file.seekg(0); + + std::vector data(total_size); + file.read(reinterpret_cast(data.data()), total_size); + + return load_from_memory(data.data(), total_size, progress_callback); + } + + static Trace load_from_memory(const uint8_t *data, size_t total_size, + std::optional> progress_callback = std::nullopt) { + Trace trace; + + // String interning table + std::map string_to_index; + auto intern_string = [&](const std::string &s) -> size_t { + auto it = string_to_index.find(s); + if (it != string_to_index.end()) { + return it->second; + } + size_t idx = trace.strings_.size(); + trace.strings_.push_back(s); + string_to_index[s] = idx; + return idx; + }; + + // Pipeline tracking for qualified names + std::map parent_to_pipeline; + // For DAG inference: packet_id -> (event, qualified_name, parent_id) + std::map> id_to_info; + // LOADs to process for DAG + std::vector> load_packets; + // Funcs with static bounds from tags + std::set funcs_with_static_bounds; + + size_t pos = 0; + size_t last_progress = 0; + const size_t progress_interval = std::max(size_t(1), total_size / 100); + + while (pos + sizeof(halide_trace_packet_t) <= total_size) { + const auto *pkt_ptr = reinterpret_cast(data + pos); + + if (pkt_ptr->size < sizeof(halide_trace_packet_t) || pos + pkt_ptr->size > total_size) { + break; + } + + // Use the halide_trace_packet_t helper methods + std::string func_name(pkt_ptr->func()); + std::string trace_tag(pkt_ptr->trace_tag()); + const auto ev = static_cast(pkt_ptr->event); + + // Track pipeline hierarchy + if (ev == halide_trace_begin_pipeline) { + trace.pipelines_[pkt_ptr->id] = func_name; + parent_to_pipeline[pkt_ptr->id] = func_name; + } else if (ev == halide_trace_end_pipeline) { + parent_to_pipeline.erase(pkt_ptr->parent_id); + } else if (parent_to_pipeline.count(pkt_ptr->parent_id)) { + parent_to_pipeline[pkt_ptr->id] = parent_to_pipeline[pkt_ptr->parent_id]; + } + + // Build qualified name + std::string qualified; + auto pipeline_it = parent_to_pipeline.find(pkt_ptr->parent_id); + if (pipeline_it != parent_to_pipeline.end() && !pipeline_it->second.empty()) { + qualified = pipeline_it->second + ":" + func_name; + } else { + qualified = func_name; + } + + // Record for DAG inference + id_to_info[pkt_ptr->id] = {pkt_ptr->event, qualified, pkt_ptr->parent_id}; + + // Handle event types + if (ev == halide_trace_tag && trace_tag.rfind("func_type_and_dim:", 0) == 0) { + parse_func_type_and_dim(qualified, trace_tag, trace.funcs_, funcs_with_static_bounds); + } else if (ev == halide_trace_begin_realization) { + if (trace.funcs_.find(qualified) == trace.funcs_.end()) { + trace.funcs_[qualified] = FuncStats{qualified}; + } + if (pipeline_it != parent_to_pipeline.end()) { + parent_to_pipeline[pkt_ptr->id] = pipeline_it->second; + } + } else if (ev == halide_trace_produce || ev == halide_trace_consume || + ev == halide_trace_end_produce || ev == halide_trace_end_consume) { + if (pipeline_it != parent_to_pipeline.end()) { + parent_to_pipeline[pkt_ptr->id] = pipeline_it->second; + } + } else if (ev == halide_trace_load) { + load_packets.emplace_back(func_name, pkt_ptr->parent_id); + if (trace.funcs_.find(qualified) == trace.funcs_.end()) { + trace.funcs_[qualified] = FuncStats{qualified}; + } + if (funcs_with_static_bounds.find(qualified) == funcs_with_static_bounds.end()) { + update_stats_inline(pkt_ptr, trace.funcs_[qualified]); + } + } else if (ev == halide_trace_store) { + if (trace.funcs_.find(qualified) == trace.funcs_.end()) { + trace.funcs_[qualified] = FuncStats{qualified}; + } + if (funcs_with_static_bounds.find(qualified) == funcs_with_static_bounds.end()) { + update_stats_inline(pkt_ptr, trace.funcs_[qualified]); + } + } + + // Build packet + TracePacket pkt; + pkt.id = pkt_ptr->id; + pkt.event = pkt_ptr->event; + pkt.parent_id = pkt_ptr->parent_id; + pkt.value_index = pkt_ptr->value_index; + pkt.type_code = pkt_ptr->type.code; + pkt.type_bits = pkt_ptr->type.bits; + pkt.type_lanes = pkt_ptr->type.lanes; + + // Copy coordinates using the helper method + if (pkt_ptr->dimensions > 0) { + pkt.coordinates.resize(pkt_ptr->dimensions); + std::memcpy(pkt.coordinates.data(), pkt_ptr->coordinates(), + pkt_ptr->dimensions * sizeof(int32_t)); + } + + // Copy value bytes using the helper method + const size_t value_bytes = pkt_ptr->type.lanes * pkt_ptr->type.bytes(); + if (value_bytes > 0) { + pkt.value.resize(value_bytes); + std::memcpy(pkt.value.data(), pkt_ptr->value(), value_bytes); + } + + // Intern strings + pkt.func = trace.strings_[intern_string(func_name)]; + if (!trace_tag.empty()) { + pkt.trace_tag = trace.strings_[intern_string(trace_tag)]; + } + + trace.packets_.push_back(std::move(pkt)); + + pos += pkt_ptr->size; + + // Progress callback + if (progress_callback && pos - last_progress >= progress_interval) { + (*progress_callback)(pos, total_size); + last_progress = pos; + } + } + + // DAG inference + for (const auto &[func_name, load_parent_id] : load_packets) { + auto pipeline_it = parent_to_pipeline.find(load_parent_id); + std::string loaded_func; + if (pipeline_it != parent_to_pipeline.end() && !pipeline_it->second.empty()) { + loaded_func = pipeline_it->second + ":" + func_name; + } else { + loaded_func = func_name; + } + + int32_t current_id = load_parent_id; + while (id_to_info.count(current_id)) { + const auto &[ev, producing_func, next_parent] = id_to_info[current_id]; + if (ev == halide_trace_produce) { + if (loaded_func != producing_func) { + trace.dag_edges_[loaded_func].insert(producing_func); + } + break; + } + current_id = next_parent; + } + } + + if (progress_callback) { + (*progress_callback)(total_size, total_size); + } + + return trace; + } + + size_t size() const { + return packets_.size(); + } + + const TracePacket &operator[](size_t i) const { + if (i >= packets_.size()) { + throw std::out_of_range("Packet index out of range"); + } + return packets_[i]; + } + + const std::map &funcs() const { + return funcs_; + } + const std::map &pipelines() const { + return pipelines_; + } + const std::map> &dag_edges() const { + return dag_edges_; + } + const std::vector &packets() const { + return packets_; + } + + std::vector filter_loads_stores() const { + std::vector result; + for (const auto &p : packets_) { + if (p.is_load_or_store()) { + result.push_back(p); + } + } + return result; + } + + std::string dag_as_dot() const { + std::ostringstream ss; + ss << "digraph dag {\n"; + ss << " rankdir=\"LR\";\n"; + ss << " node [shape=box];\n"; + + auto sanitize = [](const std::string &name) { + std::string result = name; + for (char &c : result) { + if (c == ':') c = '_'; + } + return result; + }; + + auto label = [](const std::string &name) { + auto pos = name.rfind(':'); + return (pos != std::string::npos) ? name.substr(pos + 1) : name; + }; + + for (const auto &[func, _] : funcs_) { + ss << " " << sanitize(func) << " [label=\"" << label(func) << "\"];\n"; + } + + for (const auto &[src, dsts] : dag_edges_) { + for (const auto &dst : dsts) { + ss << " " << sanitize(src) << " -> " << sanitize(dst) << ";\n"; + } + } + + ss << "}\n"; + return ss.str(); + } + +private: + std::vector packets_; + std::map funcs_; + std::map pipelines_; + std::map> dag_edges_; + std::vector strings_; // Interned strings + + static void parse_func_type_and_dim(const std::string &qualified, + const std::string &trace_tag, + std::map &funcs, + std::set &funcs_with_static_bounds) { + std::istringstream iss(trace_tag); + std::string prefix; + iss >> prefix; // "func_type_and_dim:" + + int num_types; + if (!(iss >> num_types)) return; + + // Skip type info + for (int i = 0; i < num_types * 3; ++i) { + int dummy; + if (!(iss >> dummy)) return; + } + + int num_dims; + if (!(iss >> num_dims)) return; + + std::vector min_coords, max_coords; + for (int i = 0; i < num_dims; ++i) { + int min_val, extent; + if (!(iss >> min_val >> extent)) break; + min_coords.push_back(min_val); + max_coords.push_back(min_val + extent); + } + + if (!min_coords.empty()) { + if (funcs.find(qualified) == funcs.end()) { + funcs[qualified] = FuncStats{qualified}; + } + funcs[qualified].min_coords = std::move(min_coords); + funcs[qualified].max_coords = std::move(max_coords); + funcs_with_static_bounds.insert(qualified); + } + } + + static void update_stats_inline(const halide_trace_packet_t *pkt, + FuncStats &stats) { + // Update coordinate ranges using the helper method + if (pkt->dimensions > 0) { + const int *coords = pkt->coordinates(); + if (stats.min_coords.empty()) { + stats.min_coords.resize(pkt->dimensions); + stats.max_coords.resize(pkt->dimensions); + for (int i = 0; i < pkt->dimensions; ++i) { + stats.min_coords[i] = coords[i]; + stats.max_coords[i] = coords[i] + 1; + } + } else { + for (int i = 0; i < pkt->dimensions && i < static_cast(stats.min_coords.size()); ++i) { + stats.min_coords[i] = std::min(stats.min_coords[i], coords[i]); + stats.max_coords[i] = std::max(stats.max_coords[i], coords[i] + 1); + } + } + } + + // Update value ranges using the helper method + const uint8_t *val_ptr = static_cast(pkt->value()); + const size_t elem_size = pkt->type.bytes(); + + for (uint16_t i = 0; i < pkt->type.lanes; ++i) { + double val = 0; + const uint8_t *ptr = val_ptr + i * elem_size; + + if (pkt->type.code == halide_type_float) { + if (pkt->type.bits == 32) { + float v; + std::memcpy(&v, ptr, sizeof(v)); + val = v; + } else if (pkt->type.bits == 64) { + std::memcpy(&val, ptr, sizeof(val)); + } else { + continue; + } + } else if (pkt->type.code == halide_type_int) { + if (pkt->type.bits == 8) { + val = static_cast(*ptr); + } else if (pkt->type.bits == 16) { + int16_t v; + std::memcpy(&v, ptr, sizeof(v)); + val = v; + } else if (pkt->type.bits == 32) { + int32_t v; + std::memcpy(&v, ptr, sizeof(v)); + val = v; + } else if (pkt->type.bits == 64) { + int64_t v; + std::memcpy(&v, ptr, sizeof(v)); + val = static_cast(v); + } else { + continue; + } + } else if (pkt->type.code == halide_type_uint) { + if (pkt->type.bits == 8) { + val = *ptr; + } else if (pkt->type.bits == 16) { + uint16_t v; + std::memcpy(&v, ptr, sizeof(v)); + val = v; + } else if (pkt->type.bits == 32) { + uint32_t v; + std::memcpy(&v, ptr, sizeof(v)); + val = v; + } else if (pkt->type.bits == 64) { + uint64_t v; + std::memcpy(&v, ptr, sizeof(v)); + val = static_cast(v); + } else { + continue; + } + } else { + continue; + } + + if (!stats.min_value.has_value()) { + stats.min_value = val; + stats.max_value = val; + } else { + stats.min_value = std::min(*stats.min_value, val); + stats.max_value = std::max(*stats.max_value, val); + } + } + } +}; + +void define_trace(py::module &m) { + py::class_(m, "FuncStats") + .def_readonly("name", &FuncStats::name) + .def_readonly("min_coords", &FuncStats::min_coords) + .def_readonly("max_coords", &FuncStats::max_coords) + .def_property_readonly("min_value", [](const FuncStats &s) -> py::object { + return s.min_value.has_value() ? py::cast(*s.min_value) : py::none(); + }) + .def_property_readonly("max_value", [](const FuncStats &s) -> py::object { + return s.max_value.has_value() ? py::cast(*s.max_value) : py::none(); + }); + + py::class_(m, "TracePacket") + .def_readonly("id", &TracePacket::id) + .def_readonly("event", &TracePacket::event) + .def_readonly("parent_id", &TracePacket::parent_id) + .def_readonly("value_index", &TracePacket::value_index) + .def_readonly("type_code", &TracePacket::type_code) + .def_readonly("type_bits", &TracePacket::type_bits) + .def_readonly("type_lanes", &TracePacket::type_lanes) + .def_readonly("coordinates", &TracePacket::coordinates) + .def_readonly("func", &TracePacket::func) + .def_readonly("trace_tag", &TracePacket::trace_tag) + .def_property_readonly("is_load", &TracePacket::is_load) + .def_property_readonly("is_store", &TracePacket::is_store) + .def_property_readonly("is_load_or_store", &TracePacket::is_load_or_store) + .def("get_values", &TracePacket::get_values); + + py::class_(m, "Trace") + .def_static("load", [](const std::string &path, py::object progress_callback) { + if (progress_callback.is_none()) { + return Trace::load(path); + } + return Trace::load(path, [&](size_t bytes_read, size_t total_bytes) { + progress_callback(bytes_read, total_bytes); + }); }, py::arg("path"), py::arg("progress_callback") = py::none()) + .def_static("load_bytes", [](py::bytes data) { + std::string str = data; + return Trace::load_from_memory( + reinterpret_cast(str.data()), + str.size(), + std::nullopt); }, py::arg("data")) + .def("__len__", &Trace::size) + .def("__getitem__", &Trace::operator[], py::arg("index")) + .def_property_readonly("funcs", &Trace::funcs) + .def_property_readonly("pipelines", &Trace::pipelines) + .def_property_readonly("dag_edges", &Trace::dag_edges) + .def_property_readonly("packets", &Trace::packets) + .def("filter_loads_stores", &Trace::filter_loads_stores) + .def("dag_as_dot", &Trace::dag_as_dot); +} + +} // namespace Halide::PythonBindings diff --git a/python_bindings/src/halide/halide_/PyTrace.h b/python_bindings/src/halide/halide_/PyTrace.h new file mode 100644 index 000000000000..513cbef72da3 --- /dev/null +++ b/python_bindings/src/halide/halide_/PyTrace.h @@ -0,0 +1,14 @@ +#ifndef HALIDE_PYTHON_BINDINGS_PYTRACE_H +#define HALIDE_PYTHON_BINDINGS_PYTRACE_H + +#include "PyHalide.h" + +namespace Halide { +namespace PythonBindings { + +void define_trace(py::module &m); + +} // namespace PythonBindings +} // namespace Halide + +#endif // HALIDE_PYTHON_BINDINGS_PYTRACE_H \ No newline at end of file From 8e4e26a72cb7fae21f1a257291d7a38a16932aef Mon Sep 17 00:00:00 2001 From: Alex Reinking Date: Mon, 1 Jun 2026 13:48:22 -0400 Subject: [PATCH 02/67] Add local_laplacian_trace / process_viz to build --- apps/local_laplacian/CMakeLists.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/local_laplacian/CMakeLists.txt b/apps/local_laplacian/CMakeLists.txt index 0c1580fa8ff2..7c6deeab73f1 100644 --- a/apps/local_laplacian/CMakeLists.txt +++ b/apps/local_laplacian/CMakeLists.txt @@ -36,6 +36,19 @@ add_halide_library( autoscheduler.experimental_gpu_schedule=1 ) +add_halide_library( + local_laplacian_trace + FROM local_laplacian.generator + GENERATOR local_laplacian + FEATURES trace_all + FUNCTION_NAME local_laplacian +) + +# Trace executable +add_executable(process_viz process.cpp) +target_link_libraries(process_viz PRIVATE Halide::ImageIO local_laplacian_trace) +target_compile_definitions(process_viz PRIVATE NO_AUTO_SCHEDULE) + # Main executable add_executable(local_laplacian_process process.cpp) target_link_libraries( From 4bf6c99ba0e65cfd23fb4d391b52062494fe56e4 Mon Sep 17 00:00:00 2001 From: Alex Reinking Date: Mon, 1 Jun 2026 14:39:17 -0400 Subject: [PATCH 03/67] basic neotrace fixups --- apps/neotrace/neotrace/__main__.py | 4 +- apps/neotrace/neotrace/viewer.py | 18 +- apps/neotrace/profile_load.py | 6 +- apps/neotrace/pyproject.toml | 25 +- apps/neotrace/tests/test_trace.py | 16 +- apps/neotrace/uv.lock | 770 +++++++++++++++++------------ 6 files changed, 490 insertions(+), 349 deletions(-) diff --git a/apps/neotrace/neotrace/__main__.py b/apps/neotrace/neotrace/__main__.py index caad959ca8c3..8c0b665eae49 100644 --- a/apps/neotrace/neotrace/__main__.py +++ b/apps/neotrace/neotrace/__main__.py @@ -5,7 +5,6 @@ from __future__ import annotations import argparse - import sys from pathlib import Path @@ -58,9 +57,8 @@ def main(): sys.exit(run_viewer(args.trace)) elif args.command == "info": - from tqdm import tqdm - from halide import Trace + from tqdm import tqdm last_bytes = 0 with tqdm(unit="B", unit_scale=True, unit_divisor=1024) as pbar: diff --git a/apps/neotrace/neotrace/viewer.py b/apps/neotrace/neotrace/viewer.py index cac5a3b01da2..d22beb074228 100644 --- a/apps/neotrace/neotrace/viewer.py +++ b/apps/neotrace/neotrace/viewer.py @@ -9,6 +9,7 @@ import graphviz import numpy as np +from halide import FuncStats, Trace, TracePacket from PySide6.QtCore import Qt, QTimer, Signal from PySide6.QtGui import ( QBrush, @@ -42,8 +43,6 @@ QWidget, ) -from halide import FuncStats, Trace, TracePacket - @dataclass class FuncConfig: @@ -557,7 +556,8 @@ def on_progress(bytes_read: int, total_bytes: int): self.timeline.set_time(0) self.status_bar.showMessage( - f"Loaded {path.name}: {len(trace.packets)} packets, {len(trace.funcs)} funcs" + f"Loaded {path.name}: {len(trace.packets)} packets, " + f"{len(trace.funcs)} funcs" ) except InterruptedError: self.status_bar.showMessage("Loading cancelled") @@ -848,16 +848,16 @@ def _process_store(self, packet: TracePacket): return dims_per_lane = ( - packet.dimensions // packet.type.lanes - if packet.type.lanes > 0 - else packet.dimensions + len(packet.coordinates) // packet.type_lanes + if packet.type_lanes > 0 + else len(packet.coordinates) ) - for lane in range(packet.type.lanes): + for lane in range(packet.type_lanes): # Get coordinates for this lane if dims_per_lane >= 2: - x = packet.coordinates[0 * packet.type.lanes + lane] - y = packet.coordinates[1 * packet.type.lanes + lane] + x = packet.coordinates[0 * packet.type_lanes + lane] + y = packet.coordinates[1 * packet.type_lanes + lane] elif dims_per_lane == 1: x = packet.coordinates[lane] y = 0 diff --git a/apps/neotrace/profile_load.py b/apps/neotrace/profile_load.py index a62f85375c40..65a4207c2bf5 100644 --- a/apps/neotrace/profile_load.py +++ b/apps/neotrace/profile_load.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Profile trace loading to identify bottlenecks.""" import cProfile @@ -17,7 +16,10 @@ def main(): print(f"File not found: {trace_path}") sys.exit(1) - print(f"Profiling load of {trace_path} ({trace_path.stat().st_size / 1024 / 1024:.1f} MB)") + print( + f"Profiling load of {trace_path} " + f"({trace_path.stat().st_size / 1024 / 1024:.1f} MB)" + ) print() from neotrace.trace import Trace diff --git a/apps/neotrace/pyproject.toml b/apps/neotrace/pyproject.toml index 856a63cecac6..d17533630116 100644 --- a/apps/neotrace/pyproject.toml +++ b/apps/neotrace/pyproject.toml @@ -10,12 +10,12 @@ readme = "README.md" requires-python = ">=3.10" license = "MIT" dependencies = [ - "PySide6>=6.5", - "graphviz>=0.20", - "halide", - "imageio[ffmpeg]>=2.31", - "numpy>=1.24", - "tqdm>=4.67.1", + "PySide6>=6.5", + "graphviz>=0.20", + "halide", + "imageio[ffmpeg]>=2.31", + "numpy>=1.24", + "tqdm>=4.67.1", ] [project.scripts] @@ -27,15 +27,20 @@ target-version = "py310" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM"] +ignore = ["B905"] # zip strictness [tool.uv.sources] halide = { path = "../.." } +imageio = { index = "piwheels", marker = "platform_machine == 'armv8l' or platform_machine == 'armv7l'" } +numpy = { index = "piwheels", marker = "platform_machine == 'armv8l' or platform_machine == 'armv7l'" } + +[[tool.uv.index]] +name = "piwheels" +url = "https://piwheels.org/simple" +explicit = true [tool.hatch.build.targets.wheel] packages = ["neotrace"] [dependency-groups] -dev = [ - "pytest>=9.0.2", - "ruff>=0.14.13", -] +dev = ["pytest>=9.0.2", "ruff>=0.14.13"] diff --git a/apps/neotrace/tests/test_trace.py b/apps/neotrace/tests/test_trace.py index 8228dc4c7852..a27151e39ef4 100644 --- a/apps/neotrace/tests/test_trace.py +++ b/apps/neotrace/tests/test_trace.py @@ -2,23 +2,21 @@ import struct - -def test_imports(): - """Test that all modules can be imported.""" - from neotrace.trace import EventCode, FuncStats, HalideType, Trace, TypeCode - from neotrace.viewer import TraceViewer +from neotrace.trace import ( + _NATIVE_TRACE_AVAILABLE, + EventCode, + HalideType, + Trace, + TypeCode, +) def test_native_availability(): """Report which trace implementation is being used.""" - from neotrace.trace import _NATIVE_TRACE_AVAILABLE # This test just reports the status, doesn't assert print(f"Native trace module available: {_NATIVE_TRACE_AVAILABLE}") -from neotrace.trace import EventCode, HalideType, Trace, TypeCode, _NATIVE_TRACE_AVAILABLE - - def make_packet( packet_id: int, event: EventCode, diff --git a/apps/neotrace/uv.lock b/apps/neotrace/uv.lock index 7491b4809f65..4bcfda66f07b 100644 --- a/apps/neotrace/uv.lock +++ b/apps/neotrace/uv.lock @@ -2,8 +2,10 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version < '3.11'", + "(python_full_version >= '3.11' and platform_machine == 'armv7l') or (python_full_version >= '3.11' and platform_machine == 'armv8l')", + "(python_full_version < '3.11' and platform_machine == 'armv7l') or (python_full_version < '3.11' and platform_machine == 'armv8l')", + "python_full_version >= '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", + "python_full_version < '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", ] [[package]] @@ -40,21 +42,84 @@ wheels = [ name = "halide" source = { directory = "../../" } dependencies = [ - { name = "imageio" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "imageio", version = "2.37.3", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, + { name = "imageio", version = "2.37.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://piwheels.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine == 'armv7l') or (python_full_version < '3.11' and platform_machine == 'armv8l')" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://piwheels.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine == 'armv7l') or (python_full_version >= '3.11' and platform_machine == 'armv8l')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "pillow", version = "12.2.0", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, ] [package.metadata] requires-dist = [ - { name = "imageio", specifier = ">=2" }, - { name = "numpy", specifier = ">=1.26" }, + { name = "imageio", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=2" }, + { name = "imageio", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=2", index = "https://piwheels.org/simple" }, + { name = "numpy", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=1.26" }, + { name = "numpy", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=1.26", index = "https://piwheels.org/simple" }, + { name = "pillow", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", index = "https://piwheels.org/simple" }, ] [package.metadata.requires-dev] apps = [ - { name = "onnx", specifier = ">=1.18.0" }, - { name = "pytest" }, + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, +] +ci-base = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "pre-commit", specifier = ">=4" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, + { name = "tbump", specifier = ">=6.11" }, +] +ci-llvm-21 = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "halide-llvm", specifier = "~=21.1.0", index = "https://pypi.halide-lang.org/simple" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "pre-commit", specifier = ">=4" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, + { name = "tbump", specifier = ">=6.11" }, +] +ci-llvm-22 = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "halide-llvm", specifier = "~=22.1.0", index = "https://pypi.halide-lang.org/simple" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "pre-commit", specifier = ">=4" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, + { name = "tbump", specifier = ">=6.11" }, +] +ci-llvm-main = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "halide-llvm", specifier = "~=23.0.0.dev0", index = "https://pypi.halide-lang.org/simple" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "pre-commit", specifier = ">=4" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, + { name = "tbump", specifier = ">=6.11" }, ] dev = [ { name = "pybind11", specifier = ">=2.11.1" }, @@ -63,29 +128,57 @@ dev = [ ] tools = [ { name = "cmake", specifier = ">=3.28" }, - { name = "ninja", specifier = ">=1.11" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "pre-commit", specifier = ">=4" }, { name = "ruff", specifier = ">=0.12" }, { name = "tbump", specifier = ">=6.11" }, ] [[package]] name = "imageio" -version = "2.37.2" +version = "2.37.3" +source = { registry = "https://piwheels.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine == 'armv7l') or (python_full_version >= '3.11' and platform_machine == 'armv8l')", + "(python_full_version < '3.11' and platform_machine == 'armv7l') or (python_full_version < '3.11' and platform_machine == 'armv8l')", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://piwheels.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine == 'armv7l') or (python_full_version < '3.11' and platform_machine == 'armv8l')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://piwheels.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine == 'armv7l') or (python_full_version >= '3.11' and platform_machine == 'armv8l')" }, + { name = "pillow", version = "12.2.0", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, +] +wheels = [ + { url = "https://piwheels.org/simple/imageio/imageio-2.37.3-py3-none-any.whl", hash = "sha256:06c1f430a489e305a69e006b6877451fdffe2c506099e9792d94fae3bb69cd7a" }, +] + +[package.optional-dependencies] +ffmpeg = [ + { name = "imageio-ffmpeg", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, + { name = "psutil", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, +] + +[[package]] +name = "imageio" +version = "2.37.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", + "python_full_version < '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", +] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pillow" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "pillow", version = "12.2.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, ] [package.optional-dependencies] ffmpeg = [ - { name = "imageio-ffmpeg" }, - { name = "psutil" }, + { name = "imageio-ffmpeg", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "psutil", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, ] [[package]] @@ -118,9 +211,12 @@ source = { editable = "." } dependencies = [ { name = "graphviz" }, { name = "halide" }, - { name = "imageio", extra = ["ffmpeg"] }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "imageio", version = "2.37.3", source = { registry = "https://piwheels.org/simple" }, extra = ["ffmpeg"], marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, + { name = "imageio", version = "2.37.3", source = { registry = "https://pypi.org/simple" }, extra = ["ffmpeg"], marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://piwheels.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine == 'armv7l') or (python_full_version < '3.11' and platform_machine == 'armv8l')" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://piwheels.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine == 'armv7l') or (python_full_version >= '3.11' and platform_machine == 'armv8l')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, { name = "pyside6" }, { name = "tqdm" }, ] @@ -135,8 +231,10 @@ dev = [ requires-dist = [ { name = "graphviz", specifier = ">=0.20" }, { name = "halide", directory = "../../" }, - { name = "imageio", extras = ["ffmpeg"], specifier = ">=2.31" }, - { name = "numpy", specifier = ">=1.24" }, + { name = "imageio", extras = ["ffmpeg"], marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=2.31" }, + { name = "imageio", extras = ["ffmpeg"], marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=2.31", index = "https://piwheels.org/simple" }, + { name = "numpy", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=1.24" }, + { name = "numpy", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=1.24", index = "https://piwheels.org/simple" }, { name = "pyside6", specifier = ">=6.5" }, { name = "tqdm", specifier = ">=4.67.1" }, ] @@ -147,12 +245,24 @@ dev = [ { name = "ruff", specifier = ">=0.14.13" }, ] +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://piwheels.org/simple" } +resolution-markers = [ + "(python_full_version < '3.11' and platform_machine == 'armv7l') or (python_full_version < '3.11' and platform_machine == 'armv8l')", +] +wheels = [ + { url = "https://piwheels.org/simple/numpy/numpy-2.2.6-cp311-cp311-linux_armv7l.whl", hash = "sha256:9d5168644d4f18fda98839f03a5e6ede9b1b780ac4a187c188971a3dc4b0382e" }, + { url = "https://piwheels.org/simple/numpy/numpy-2.2.6-cp313-cp313-linux_armv7l.whl", hash = "sha256:495b42b52f5be434ff56e549d9deac19ac50de272f1f803755408e21c5e5e8cc" }, +] + [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ @@ -214,191 +324,219 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.1" +version = "2.4.6" +source = { registry = "https://piwheels.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine == 'armv7l') or (python_full_version >= '3.11' and platform_machine == 'armv8l')", +] +wheels = [ + { url = "https://piwheels.org/simple/numpy/numpy-2.4.6-cp311-cp311-linux_armv7l.whl", hash = "sha256:a74f04fb7f02e62d9d2f3c9758a5dab22ebefda6f87d6c703b9f968b06485fbb" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11'", + "python_full_version >= '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", ] -sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, - { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, - { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, - { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, - { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, - { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, - { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, - { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, - { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, - { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, - { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, - { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, - { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, - { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, - { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, - { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, - { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, - { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, - { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, - { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, - { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" }, - { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, - { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, - { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, - { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, - { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, - { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, - { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, - { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, - { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, - { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, - { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, - { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, - { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, - { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, - { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, - { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, ] [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "pillow" -version = "12.1.0" +version = "12.2.0" +source = { registry = "https://piwheels.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine == 'armv7l') or (python_full_version >= '3.11' and platform_machine == 'armv8l')", + "(python_full_version < '3.11' and platform_machine == 'armv7l') or (python_full_version < '3.11' and platform_machine == 'armv8l')", +] +wheels = [ + { url = "https://piwheels.org/simple/pillow/pillow-12.2.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:10f568b93c7338fdaacc2c7a6b86efb2d4a283dc3748657b60d9a9e0f2e40bb3" }, + { url = "https://piwheels.org/simple/pillow/pillow-12.2.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:d4ff90fbce9831907b35a54405d8b1312c1b83e29f72cc45cf02e60ee55dd48b" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +resolution-markers = [ + "python_full_version >= '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", + "python_full_version < '3.11' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, - { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, - { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, - { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, - { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, - { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, - { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, - { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, - { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, - { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, - { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, - { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, - { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, - { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, - { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, - { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, - { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, - { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, - { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, - { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, - { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, - { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, - { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, - { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, - { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, - { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, - { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, - { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, - { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, - { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, - { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, - { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, - { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, - { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, - { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, - { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, - { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, - { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, + { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, + { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] [[package]] @@ -412,92 +550,93 @@ wheels = [ [[package]] name = "psutil" -version = "7.2.1" +version = "7.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, - { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, - { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, - { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, - { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, - { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, - { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, - { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, - { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, - { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyside6" -version = "6.10.1" +version = "6.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyside6-addons" }, { name = "pyside6-essentials" }, { name = "shiboken6" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/56/22/f82cfcd1158be502c5741fe67c3fa853f3c1edbd3ac2c2250769dd9722d1/pyside6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:d0e70dd0e126d01986f357c2a555722f9462cf8a942bf2ce180baf69f468e516", size = 558169, upload-time = "2025-11-20T10:09:08.79Z" }, - { url = "https://files.pythonhosted.org/packages/66/eb/54afe242a25d1c33b04ecd8321a549d9efb7b89eef7690eed92e98ba1dc9/pyside6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4053bf51ba2c2cb20e1005edd469997976a02cec009f7c46356a0b65c137f1fa", size = 557818, upload-time = "2025-11-20T10:09:10.132Z" }, - { url = "https://files.pythonhosted.org/packages/4d/af/5706b1b33587dc2f3dfa3a5000424befba35e4f2d5889284eebbde37138b/pyside6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7d3ca20a40139ca5324a7864f1d91cdf2ff237e11bd16354a42670f2a4eeb13c", size = 558358, upload-time = "2025-11-20T10:09:11.288Z" }, - { url = "https://files.pythonhosted.org/packages/26/41/3f48d724ecc8e42cea8a8442aa9b5a86d394b85093275990038fd1020039/pyside6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9f89ff994f774420eaa38cec6422fddd5356611d8481774820befd6f3bb84c9e", size = 564424, upload-time = "2025-11-20T10:09:12.677Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/395411473b433875a82f6b5fdd0cb28f19a0e345bcaac9fbc039400d7072/pyside6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:9c5c1d94387d1a32a6fae25348097918ef413b87dfa3767c46f737c6d48ae437", size = 548866, upload-time = "2025-11-20T10:09:14.174Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/27ba5947ed48918f7b74b7c43a1e280aac069e36f25adeb4c9adfac835c4/pyside6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:537682c3b7530817203e667c1f5a2f00486b37bf52c52eeab438544c7a0917f6", size = 571921, upload-time = "2026-05-13T09:47:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/d8/de/af89d71410c83b10654d86ff9aff2a4f87c30163658f1cc145242e222526/pyside6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b1fc521ba2bb5109425ab8add06bddbdd524abcad06cfa012cc39a22a189feb2", size = 572102, upload-time = "2026-05-13T09:47:38.249Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0e/d583bd3f7bf5046a4497b36f3902cfb64aa29554489a5a25c18e6b4ac0ac/pyside6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:75f0005c3eb95c07cfb65522ec50d0815ac007a96482c21dc3cb4b4c04895d84", size = 572098, upload-time = "2026-05-13T09:47:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/57/f2/d9d8ce1373dabb37e5919f63cd18446556079631d3f2eea3ada03c29f6b8/pyside6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0968877ab1fb4ef3587a284da6fe05e8647ada56a6a3750b6395188e01f4aba6", size = 578377, upload-time = "2026-05-13T09:47:40.76Z" }, + { url = "https://files.pythonhosted.org/packages/96/02/a6057d8bd2bdb1940820fff2d627fdf4013148c9c57adf69fa40d3452ac3/pyside6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:acee467cb5f256cc47ebb9d815a054c1d8416da380c191b247a76d164aa3f805", size = 561765, upload-time = "2026-05-13T09:47:41.9Z" }, ] [[package]] name = "pyside6-addons" -version = "6.10.1" +version = "6.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyside6-essentials" }, { name = "shiboken6" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/f9/b72a2578d7dbef7741bb90b5756b4ef9c99a5b40148ea53ce7f048573fe9/pyside6_addons-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4d2b82bbf9b861134845803837011e5f9ac7d33661b216805273cf0c6d0f8e82", size = 322639446, upload-time = "2025-11-20T09:54:50.75Z" }, - { url = "https://files.pythonhosted.org/packages/94/3b/3ed951c570a15570706a89d39bfd4eaaffdf16d5c2dca17e82fc3ec8aaa6/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:330c229b58d30083a7b99ed22e118eb4f4126408429816a4044ccd0438ae81b4", size = 170678293, upload-time = "2025-11-20T09:56:40.991Z" }, - { url = "https://files.pythonhosted.org/packages/22/77/4c780b204d0bf3323a75c184e349d063e208db44c993f1214aa4745d6f47/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:56864b5fecd6924187a2d0f7e98d968ed72b6cc267caa5b294cd7e88fff4e54c", size = 166365011, upload-time = "2025-11-20T09:57:20.261Z" }, - { url = "https://files.pythonhosted.org/packages/04/14/58239776499e6b279fa6ca2e0d47209531454b99f6bd2ad7c96f11109416/pyside6_addons-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:b6e249d15407dd33d6a2ffabd9dc6d7a8ab8c95d05f16a71dad4d07781c76341", size = 164864664, upload-time = "2025-11-20T09:57:54.815Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cd/1b74108671ba4b1ebb2661330665c4898b089e9c87f7ba69fe2438f3d1b6/pyside6_addons-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:0de303c0447326cdc6c8be5ab066ef581e2d0baf22560c9362d41b8304fdf2db", size = 34191225, upload-time = "2025-11-20T09:58:04.184Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/8bc94aff48b63f788f2d84e5467c12362d68906ba742c0942f46cb04c879/pyside6_addons-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:54733c77f789bef5f03c6aff4ad3bec8b2eff021f0cfcbc53d5e6c250ded24f9", size = 331714589, upload-time = "2026-05-13T09:39:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/fb1428a523b2a4541e232aab50d9e789e6b4526f37fd9593452a7ea5b6b3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6c65fbd73a512d6f72cda8d8277444a85a34dc99dd1dae9c21d35b8671bb1f", size = 175063224, upload-time = "2026-05-13T09:39:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9b/2ccd52f66db55c06de65d0501170a1935d04d64d0a230c0d892284a02ce3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:bf1c6c4e954e5eba3d2a7c661ad4b9689e8f09c7f4a16bdf29713371d11af993", size = 170553429, upload-time = "2026-05-13T09:39:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bd/8adc4d350b3b363f3dfc8fccdcf5bfed25f7e36c2fff30c64e106f4f1572/pyside6_addons-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0d13c4dfd671b050a48e4f8d8ddc724b7248f9c0437e7fc47fdf316278572923", size = 168816308, upload-time = "2026-05-13T09:40:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/65/b7/9a840d97f0f0f04e372a87e205dd30ee285b4e3b021b188459a917c9dc76/pyside6_addons-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:3494f480dee92f415be2f2d989c0b3f4755ac332b28045cbf4ba0f5c5a22ba37", size = 35759347, upload-time = "2026-05-13T09:40:21.199Z" }, ] [[package]] name = "pyside6-essentials" -version = "6.10.1" +version = "6.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "shiboken6" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/04/b0/c43209fecef79912e9b1c70a1b5172b1edf76caebcc885c58c60a09613b0/pyside6_essentials-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:cd224aff3bb26ff1fca32c050e1c4d0bd9f951a96219d40d5f3d0128485b0bbe", size = 105461499, upload-time = "2025-11-20T09:59:23.733Z" }, - { url = "https://files.pythonhosted.org/packages/5f/8e/b69ba7fa0c701f3f4136b50460441697ec49ee6ea35c229eb2a5ee4b5952/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e9ccbfb58c03911a0bce1f2198605b02d4b5ca6276bfc0cbcf7c6f6393ffb856", size = 76764617, upload-time = "2025-11-20T09:59:38.831Z" }, - { url = "https://files.pythonhosted.org/packages/bd/83/569d27f4b6c6b9377150fe1a3745d64d02614021bea233636bc936a23423/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ec8617c9b143b0c19ba1cc5a7e98c538e4143795480cb152aee47802c18dc5d2", size = 75850373, upload-time = "2025-11-20T09:59:56.082Z" }, - { url = "https://files.pythonhosted.org/packages/1e/64/a8df6333de8ccbf3a320e1346ca30d0f314840aff5e3db9b4b66bf38e26c/pyside6_essentials-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9555a48e8f0acf63fc6a23c250808db841b28a66ed6ad89ee0e4df7628752674", size = 74491180, upload-time = "2025-11-20T10:00:11.215Z" }, - { url = "https://files.pythonhosted.org/packages/67/da/65cc6c6a870d4ea908c59b2f0f9e2cf3bfc6c0710ebf278ed72f69865e4e/pyside6_essentials-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:4d1d248644f1778f8ddae5da714ca0f5a150a5e6f602af2765a7d21b876da05c", size = 55190458, upload-time = "2025-11-20T10:00:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/b3/da/10d9197e7370eb4fed8df5fc547b7548dec88e5c5949e2d450db4ae96feb/pyside6_essentials-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:228de53c2bc26b07e5021fbe3614fc44ca08e4dab9999af08c2b389d2c239957", size = 110352945, upload-time = "2026-05-13T09:43:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/0e1237c4400bec7e335d2c4eeb49bc40d9fd88a9ac44ca9083ce1abdc308/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e3ef7027b41e4e55fadb56e3b3257dc8ee92154b639fe67fc4c8e05e9d976c60", size = 79908535, upload-time = "2026-05-13T09:43:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c5/da4c5f23c6540ac5211a1f60177c8dee84b1bf40f2719479587ab8c60731/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a039b6da68a3a4b9d243217b2b98d475eed3f617159ef6be925badab53c11b0d", size = 78960051, upload-time = "2026-05-13T09:43:35.423Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/b663ecc96ca57b5c91b83b6615d6b174380b0faf30338125c26e053d6aa7/pyside6_essentials-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:63311bd48e32c584599ab04b9ef7c324082374cd2c9fa533f978fb893bb47e40", size = 77549267, upload-time = "2026-05-13T09:43:44.92Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/eb6723faf5cb7fa581145da1c15f40d641b96e080f0491af2f1859fdeedb/pyside6_essentials-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:11253ea52aabecefe9febddbbe78b43a824129e3af1cec98431028fba7fa954f", size = 57964512, upload-time = "2026-05-13T09:43:52.968Z" }, ] [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -508,113 +647,112 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] name = "ruff" -version = "0.14.14" +version = "0.15.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, - { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, - { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, - { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, ] [[package]] name = "shiboken6" -version = "6.10.1" +version = "6.11.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/8b/e5db743d505ceea3efc4cd9634a3bee22a3e2bf6e07cefd28c9b9edabcc6/shiboken6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:9f2990f5b61b0b68ecadcd896ab4441f2cb097eef7797ecc40584107d9850d71", size = 478483, upload-time = "2025-11-20T10:08:52.411Z" }, - { url = "https://files.pythonhosted.org/packages/56/ba/b50c1a44b3c4643f482afbf1a0ea58f393827307100389ce29404f9ad3b0/shiboken6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4221a52dfb81f24a0d20cc4f8981cb6edd810d5a9fb28287ce10d342573a0e4", size = 271993, upload-time = "2025-11-20T10:08:54.093Z" }, - { url = "https://files.pythonhosted.org/packages/16/b8/939c24ebd662b0aa5c945443d0973145b3fb7079f0196274ef7bb4b98f73/shiboken6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c095b00f4d6bf578c0b2464bb4e264b351a99345374478570f69e2e679a2a1d0", size = 268691, upload-time = "2025-11-20T10:08:55.639Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a6/8c65ee0fa5e172ebcca03246b1bc3bd96cdaf1d60537316648536b7072a5/shiboken6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:c1601d3cda1fa32779b141663873741b54e797cb0328458d7466281f117b0a4e", size = 1234704, upload-time = "2025-11-20T10:08:57.417Z" }, - { url = "https://files.pythonhosted.org/packages/7b/6a/c0fea2f2ac7d9d96618c98156500683a4d1f93fea0e8c5a2bc39913d7ef1/shiboken6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:5cf800917008587b551005a45add2d485cca66f5f7ecd5b320e9954e40448cc9", size = 1795567, upload-time = "2025-11-20T10:08:59.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/f3/f2b63df0251e7cd3172ea28e32ede52739de9566bcefcd0178681538ac81/shiboken6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:1a16867f103ef1c662a5f09dfed03273a9f81688b174555162c58e83650a3f02", size = 476874, upload-time = "2026-05-13T09:47:01.091Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9b/e0355d8897b5c150770f1d95718aad17d432fcc9c035c04f3f58427d4693/shiboken6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9a8bccfafc8805254cabcfa1edfaf55cd52889f4998c91ad0d9a4433fb1bcdbe", size = 272222, upload-time = "2026-05-13T09:47:02.653Z" }, + { url = "https://files.pythonhosted.org/packages/57/d5/dd4f1defed400be03340f2ede34b61f846776650b4e7ed9ebaf4c71979a2/shiboken6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:1bd2f4314414df2d122d9f646e03b731bc6d6b5f77a5f53f99a4fe4e97d84e6f", size = 270350, upload-time = "2026-05-13T09:47:04.02Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/3f6fb2ee65b534193fb4ef713dd619dc31dadff5d12c16979a7699ad58be/shiboken6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:c2c6863aa80ec18c0f82cea3417837b279cdc60024ac17123461dc9042577df7", size = 1223647, upload-time = "2026-05-13T09:47:05.924Z" }, + { url = "https://files.pythonhosted.org/packages/98/d1/f15ca0e1666faae02c945f48e745ea35f8fcd8243b176109b4e2c4251f47/shiboken6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:7c8d9af17db4495d4fa5b1c393f218311c4855546b9dfa6a0bd21bcd66b55e9d", size = 1784170, upload-time = "2026-05-13T09:47:07.617Z" }, ] [[package]] name = "tomli" -version = "2.4.0" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] name = "tqdm" -version = "4.67.1" +version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] From 88e30e5a805268b0cce92f2fbeb7ef3d51048134 Mon Sep 17 00:00:00 2001 From: Alex Reinking Date: Mon, 1 Jun 2026 14:43:01 -0400 Subject: [PATCH 04/67] pre-commit after rebase --- apps/neotrace/.gitignore | 1 + apps/neotrace/README.md | 53 +++++++++++--------- python_bindings/src/halide/halide_/PyTrace.h | 2 +- 3 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 apps/neotrace/.gitignore diff --git a/apps/neotrace/.gitignore b/apps/neotrace/.gitignore new file mode 100644 index 000000000000..35659004cc92 --- /dev/null +++ b/apps/neotrace/.gitignore @@ -0,0 +1 @@ +*.hltrace diff --git a/apps/neotrace/README.md b/apps/neotrace/README.md index af3ae63eb15d..778dda439505 100644 --- a/apps/neotrace/README.md +++ b/apps/neotrace/README.md @@ -68,16 +68,17 @@ ruff check . pytest ``` ---- +______________________________________________________________________ ## Visualization Specification ### Data Dimensionality & Rendering Modes -Halide Funcs can have varying dimensionality. Neotrace supports multiple rendering modes: +Halide Funcs can have varying dimensionality. Neotrace supports multiple +rendering modes: | Dimensions | Examples | Default Rendering | -|---------------|---------------------------|---------------------------| +| ------------- | ------------------------- | ------------------------- | | 0D | Scalar reduction | Single value display | | 1D | Histogram, LUT | Line or wrapped rectangle | | 2D | Grayscale image | Heatmap / grayscale | @@ -88,47 +89,53 @@ Halide Funcs can have varying dimensionality. Neotrace supports multiple renderi #### Rendering Modes 1. **Grayscale / Heatmap Mode** (1D, 2D) - - Maps scalar values to color via configurable colormap - - Colormaps: `grayscale`, `viridis`, `plasma`, `hot`, `cool` - - Value range: configurable `[min_value, max_value]` + + - Maps scalar values to color via configurable colormap + - Colormaps: `grayscale`, `viridis`, `plasma`, `hot`, `cool` + - Value range: configurable `[min_value, max_value]` 2. **RGB Mode** (2D + channel dimension) - - Interprets one dimension as color channels (R, G, B, optionally A) - - Channel dimension detected heuristically (dimension with extent 3 or 4) - - Value ranges: `uint8` 0-255, `float32` 0.0-1.0 (configurable) + + - Interprets one dimension as color channels (R, G, B, optionally A) + - Channel dimension detected heuristically (dimension with extent 3 or 4) + - Value ranges: `uint8` 0-255, `float32` 0.0-1.0 (configurable) 3. **Line Mode** (1D) - - Renders 1D data as a horizontal or vertical line - - Height/width configurable (default: 16px) + + - Renders 1D data as a horizontal or vertical line + - Height/width configurable (default: 16px) 4. **Wrapped Mode** (1D) - - Wraps 1D data into a 2D rectangle - - Wrap width auto-computed to approximate square + + - Wraps 1D data into a 2D rectangle + - Wrap width auto-computed to approximate square 5. **Projected Mode** (3D+) - - Reduces higher dimensions by fixing indices - - User specifies which indices to hold constant - - Example: 4D `[batch, y, x, c]` with `batch=0` → RGB image + + - Reduces higher dimensions by fixing indices + - User specifies which indices to hold constant + - Example: 4D `[batch, y, x, c]` with `batch=0` → RGB image 6. **Tiled Mode** (3D+) - - Arranges slices in a grid - - User specifies base visualization dims and tiling dims - - Example: `[z, y, x]` → z slices arranged in grid + + - Arranges slices in a grid + - User specifies base visualization dims and tiling dims + - Example: `[z, y, x]` → z slices arranged in grid ### Load & Store Visualization - **Stores**: Solid color based on value (current behavior) - **Loads**: Configurable visual treatment: - - `outline`: Border around accessed pixels - - `heatmap`: Overlay showing access frequency - - `flash`: Brief highlight animation during playback + - `outline`: Border around accessed pixels + - `heatmap`: Overlay showing access frequency + - `flash`: Brief highlight animation during playback ### Liveness Visualization Funcs are visualized based on their liveness state: | State | Condition | Visual Treatment | -|------------|-----------------------------------|--------------------------| +| ---------- | --------------------------------- | ------------------------ | | **Unborn** | Current time < first store | Grayed out (20% opacity) | | **Alive** | Between first store and last load | Full opacity | | **Dead** | Current time > last load | Faded (40% opacity) | diff --git a/python_bindings/src/halide/halide_/PyTrace.h b/python_bindings/src/halide/halide_/PyTrace.h index 513cbef72da3..da7c5f0fdd57 100644 --- a/python_bindings/src/halide/halide_/PyTrace.h +++ b/python_bindings/src/halide/halide_/PyTrace.h @@ -11,4 +11,4 @@ void define_trace(py::module &m); } // namespace PythonBindings } // namespace Halide -#endif // HALIDE_PYTHON_BINDINGS_PYTRACE_H \ No newline at end of file +#endif // HALIDE_PYTHON_BINDINGS_PYTRACE_H From d20791ceccc0bb075c349f6e35ae1eb3860d501c Mon Sep 17 00:00:00 2001 From: Alex Reinking Date: Mon, 1 Jun 2026 17:37:16 -0400 Subject: [PATCH 05/67] Add profiling; fix render performance --- apps/neotrace/neotrace/viewer.py | 86 +++++++++++++++---- .../src/halide/halide_/PyTrace.cpp | 17 +--- 2 files changed, 74 insertions(+), 29 deletions(-) diff --git a/apps/neotrace/neotrace/viewer.py b/apps/neotrace/neotrace/viewer.py index d22beb074228..286305e02ca0 100644 --- a/apps/neotrace/neotrace/viewer.py +++ b/apps/neotrace/neotrace/viewer.py @@ -4,6 +4,7 @@ from __future__ import annotations +import time as _time from dataclasses import dataclass, field from pathlib import Path @@ -437,6 +438,8 @@ def __init__(self): # Cache for func name lookups self._func_name_cache: dict[str, FuncItem | None] = {} + # Cached packets list — trace.packets copies the C++ vector every access + self._packets: list[TracePacket] = [] self._setup_ui() self._setup_menu() @@ -533,6 +536,9 @@ def on_progress(bytes_read: int, total_bytes: int): self.state.trace = trace self.state.current_time = -1 # Reset so first render processes all packets + self._packets = ( + trace.packets + ) # Cache once; avoids full C++ vector copy each access # Clear func name cache self._func_name_cache = {} @@ -542,7 +548,7 @@ def on_progress(bytes_read: int, total_bytes: int): # Update UI self.func_list.set_funcs(trace.funcs) - self.timeline.set_range(len(trace.packets) - 1) + self.timeline.set_range(len(self._packets) - 1) # Add func items to canvas self.canvas.clear_funcs() @@ -556,7 +562,7 @@ def on_progress(bytes_read: int, total_bytes: int): self.timeline.set_time(0) self.status_bar.showMessage( - f"Loaded {path.name}: {len(trace.packets)} packets, " + f"Loaded {path.name}: {len(self._packets)} packets, " f"{len(trace.funcs)} funcs" ) except InterruptedError: @@ -807,33 +813,70 @@ def _render_to_time(self, time: int): last_time = self.state.current_time + self._prof_get_values = 0.0 + self._prof_update_pixel = 0.0 + self._prof_coord = 0.0 + t0 = _time.perf_counter() + # Determine rendering strategy - if time > last_time and last_time >= 0: - # Moving forward: incremental update - self._render_range(last_time + 1, time + 1) + if time > last_time: + if last_time < 0: + # Starting fresh: render from the beginning + n_stores = self._render_range(0, time + 1) + else: + # Moving forward: incremental update + n_stores = self._render_range(last_time + 1, time + 1) elif time < last_time: # Moving backward: must re-render from scratch for item in self.canvas.func_items.values(): item._init_data_array() - self._render_range(0, time + 1) - # else: time == last_time, nothing to do + n_stores = self._render_range(0, time + 1) + else: + n_stores = 0 + + t1 = _time.perf_counter() # Refresh all pixmaps for item in self.canvas.func_items.values(): item.refresh_pixmap() + t2 = _time.perf_counter() + self.state.current_time = time - def _render_range(self, start: int, end: int): - """Process packets in the given range [start, end).""" + import sys + + n_packets = abs(time - last_time) + gv = 1000 * self._prof_get_values + up = 1000 * self._prof_update_pixel + co = 1000 * self._prof_coord - up + msg = ( + f"packets={n_packets} stores={n_stores} " + f"process={1000 * (t1 - t0):.1f}ms " + f"[get_values={gv:.1f}ms coord={co:.1f}ms update_pixel={up:.1f}ms] " + f"pixmap={1000 * (t2 - t1):.1f}ms total={1000 * (t2 - t0):.1f}ms" + ) + print(msg, file=sys.stderr) + self.status_bar.showMessage(msg) + + def _render_range(self, start: int, end: int) -> int: + """Process packets in the given range [start, end). Returns store count.""" if not self.state.trace: - return + return 0 - packets = self.state.trace.packets + n_stores = 0 + packets = self._packets for i in range(start, min(end, len(packets))): packet = packets[i] if packet.is_store: self._process_store(packet) + n_stores += 1 + return n_stores + + # Profiling accumulators — reset each tick in _render_to_time + _prof_get_values: float = 0.0 + _prof_update_pixel: float = 0.0 + _prof_coord: float = 0.0 def _process_store(self, packet: TracePacket): """Process a store packet.""" @@ -843,29 +886,40 @@ def _process_store(self, packet: TracePacket): if item is None: return + ta = _time.perf_counter() values = packet.get_values() + tb = _time.perf_counter() + self._prof_get_values += tb - ta + if not values: return + tc = _time.perf_counter() dims_per_lane = ( len(packet.coordinates) // packet.type_lanes if packet.type_lanes > 0 else len(packet.coordinates) ) - for lane in range(packet.type_lanes): + coords = packet.coordinates + n_lanes = packet.type_lanes + for lane in range(n_lanes): # Get coordinates for this lane if dims_per_lane >= 2: - x = packet.coordinates[0 * packet.type_lanes + lane] - y = packet.coordinates[1 * packet.type_lanes + lane] + x = coords[0 * n_lanes + lane] + y = coords[1 * n_lanes + lane] elif dims_per_lane == 1: - x = packet.coordinates[lane] + x = coords[lane] y = 0 else: x = y = 0 if lane < len(values): + td = _time.perf_counter() item.update_pixel(x, y, values[lane], is_store=True) + self._prof_update_pixel += _time.perf_counter() - td + + self._prof_coord += _time.perf_counter() - tc def _get_func_item_for_packet(self, func_name: str) -> FuncItem | None: """Get the FuncItem for a packet's func name, with caching.""" @@ -901,7 +955,7 @@ def _on_playback_tick(self): if not self.state.trace: return - max_time = len(self.state.trace.packets) - 1 + max_time = len(self._packets) - 1 new_time = min(self.state.current_time + self._playback_step, max_time) if new_time >= max_time: diff --git a/python_bindings/src/halide/halide_/PyTrace.cpp b/python_bindings/src/halide/halide_/PyTrace.cpp index 04165d435553..4cf397d180ea 100644 --- a/python_bindings/src/halide/halide_/PyTrace.cpp +++ b/python_bindings/src/halide/halide_/PyTrace.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -148,8 +147,6 @@ class Trace { std::map> id_to_info; // LOADs to process for DAG std::vector> load_packets; - // Funcs with static bounds from tags - std::set funcs_with_static_bounds; size_t pos = 0; size_t last_progress = 0; @@ -191,7 +188,7 @@ class Trace { // Handle event types if (ev == halide_trace_tag && trace_tag.rfind("func_type_and_dim:", 0) == 0) { - parse_func_type_and_dim(qualified, trace_tag, trace.funcs_, funcs_with_static_bounds); + parse_func_type_and_dim(qualified, trace_tag, trace.funcs_); } else if (ev == halide_trace_begin_realization) { if (trace.funcs_.find(qualified) == trace.funcs_.end()) { trace.funcs_[qualified] = FuncStats{qualified}; @@ -209,16 +206,12 @@ class Trace { if (trace.funcs_.find(qualified) == trace.funcs_.end()) { trace.funcs_[qualified] = FuncStats{qualified}; } - if (funcs_with_static_bounds.find(qualified) == funcs_with_static_bounds.end()) { - update_stats_inline(pkt_ptr, trace.funcs_[qualified]); - } + update_stats_inline(pkt_ptr, trace.funcs_[qualified]); } else if (ev == halide_trace_store) { if (trace.funcs_.find(qualified) == trace.funcs_.end()) { trace.funcs_[qualified] = FuncStats{qualified}; } - if (funcs_with_static_bounds.find(qualified) == funcs_with_static_bounds.end()) { - update_stats_inline(pkt_ptr, trace.funcs_[qualified]); - } + update_stats_inline(pkt_ptr, trace.funcs_[qualified]); } // Build packet @@ -368,8 +361,7 @@ class Trace { static void parse_func_type_and_dim(const std::string &qualified, const std::string &trace_tag, - std::map &funcs, - std::set &funcs_with_static_bounds) { + std::map &funcs) { std::istringstream iss(trace_tag); std::string prefix; iss >> prefix; // "func_type_and_dim:" @@ -400,7 +392,6 @@ class Trace { } funcs[qualified].min_coords = std::move(min_coords); funcs[qualified].max_coords = std::move(max_coords); - funcs_with_static_bounds.insert(qualified); } } From 15b95ed7cab10b1798015f8ed7e6a7d4a06d62fa Mon Sep 17 00:00:00 2001 From: Alex Reinking Date: Mon, 1 Jun 2026 18:09:27 -0400 Subject: [PATCH 06/67] Improve scrubbing performance --- apps/neotrace/neotrace/viewer.py | 184 ++++++++++++------ .../src/halide/halide_/PyTrace.cpp | 14 ++ 2 files changed, 140 insertions(+), 58 deletions(-) diff --git a/apps/neotrace/neotrace/viewer.py b/apps/neotrace/neotrace/viewer.py index 286305e02ca0..adce67ffca28 100644 --- a/apps/neotrace/neotrace/viewer.py +++ b/apps/neotrace/neotrace/viewer.py @@ -4,6 +4,7 @@ from __future__ import annotations +import bisect import time as _time from dataclasses import dataclass, field from pathlib import Path @@ -436,10 +437,20 @@ def __init__(self): self._playback_timer.timeout.connect(self._on_playback_tick) self._playback_step = 10000 # Packets per tick (increased for performance) + # Scrub debounce — defer rendering until slider stops moving + self._scrub_timer = QTimer(self) + self._scrub_timer.setSingleShot(True) + self._scrub_timer.setInterval(50) + self._scrub_timer.timeout.connect(self._on_scrub_settled) + self._pending_scrub_time: int = -1 + # Cache for func name lookups self._func_name_cache: dict[str, FuncItem | None] = {} # Cached packets list — trace.packets copies the C++ vector every access self._packets: list[TracePacket] = [] + # Sorted indices of store packets — lets _render_range skip + # non-stores via bisect + self._store_indices: list[int] = [] self._setup_ui() self._setup_menu() @@ -539,6 +550,9 @@ def on_progress(bytes_read: int, total_bytes: int): self._packets = ( trace.packets ) # Cache once; avoids full C++ vector copy each access + self._store_indices = ( + trace.store_indices() + ) # Sorted indices for bisect in _render_range # Clear func name cache self._func_name_cache = {} @@ -846,80 +860,125 @@ def _render_to_time(self, time: int): import sys - n_packets = abs(time - last_time) + if time > last_time: + direction = "fwd" if last_time >= 0 else "init" + elif time < last_time: + direction = "bwd" + else: + direction = "=" + n_packets = time + 1 if direction in ("bwd", "init") else abs(time - last_time) gv = 1000 * self._prof_get_values + co = 1000 * self._prof_coord up = 1000 * self._prof_update_pixel - co = 1000 * self._prof_coord - up msg = ( - f"packets={n_packets} stores={n_stores} " + f"[{direction}] packets={n_packets} stores={n_stores} " f"process={1000 * (t1 - t0):.1f}ms " - f"[get_values={gv:.1f}ms coord={co:.1f}ms update_pixel={up:.1f}ms] " + f"[get_values={gv:.1f}ms coord={co:.1f}ms batch_write={up:.1f}ms] " f"pixmap={1000 * (t2 - t1):.1f}ms total={1000 * (t2 - t0):.1f}ms" ) print(msg, file=sys.stderr) self.status_bar.showMessage(msg) - def _render_range(self, start: int, end: int) -> int: - """Process packets in the given range [start, end). Returns store count.""" - if not self.state.trace: - return 0 - - n_stores = 0 - packets = self._packets - for i in range(start, min(end, len(packets))): - packet = packets[i] - if packet.is_store: - self._process_store(packet) - n_stores += 1 - return n_stores - # Profiling accumulators — reset each tick in _render_to_time _prof_get_values: float = 0.0 _prof_update_pixel: float = 0.0 _prof_coord: float = 0.0 - def _process_store(self, packet: TracePacket): - """Process a store packet.""" - # Use cached lookup if available - func_name = packet.func - item = self._get_func_item_for_packet(func_name) - if item is None: - return - - ta = _time.perf_counter() - values = packet.get_values() - tb = _time.perf_counter() - self._prof_get_values += tb - ta + def _render_range(self, start: int, end: int) -> int: + """ + Process packets in [start, end), batch-applying pixel writes. + Returns store count. + """ + if not self.state.trace: + return 0 - if not values: - return + # Collect pixel writes per func before touching any numpy arrays. + # pending[id(item)] = [px_list, py_list, val_list, item] + pending: dict[int, list] = {} - tc = _time.perf_counter() - dims_per_lane = ( - len(packet.coordinates) // packet.type_lanes - if packet.type_lanes > 0 - else len(packet.coordinates) - ) + n_stores = 0 + packets = self._packets + store_indices = self._store_indices + end = min(end, len(packets)) + + # Binary-search to the first store in range, then walk only store packets. + lo = bisect.bisect_left(store_indices, start) + hi = bisect.bisect_right(store_indices, end - 1) + for si in range(lo, hi): + i = store_indices[si] + packet = packets[i] - coords = packet.coordinates - n_lanes = packet.type_lanes - for lane in range(n_lanes): - # Get coordinates for this lane - if dims_per_lane >= 2: - x = coords[0 * n_lanes + lane] - y = coords[1 * n_lanes + lane] - elif dims_per_lane == 1: - x = coords[lane] - y = 0 + item = self._get_func_item_for_packet(packet.func) + if item is None: + continue + + ta = _time.perf_counter() + values = packet.get_values() + self._prof_get_values += _time.perf_counter() - ta + if not values: + continue + + tc = _time.perf_counter() + coords = packet.coordinates + n_lanes = packet.type_lanes + dims_per_lane = len(coords) // n_lanes if n_lanes > 0 else len(coords) + min_x = item.min_x + min_y = item.min_y + + key = id(item) + if key not in pending: + pending[key] = [[], [], [], item] + px_list, py_list, val_list, _ = pending[key] + + for lane in range(n_lanes): + if dims_per_lane >= 2: + x = coords[lane] - min_x # coords[0*n_lanes + lane] + y = coords[n_lanes + lane] - min_y # coords[1*n_lanes + lane] + elif dims_per_lane == 1: + x = coords[lane] - min_x + y = -min_y + else: + x = -min_x + y = -min_y + if lane < len(values): + px_list.append(x) + py_list.append(y) + val_list.append(values[lane]) + + self._prof_coord += _time.perf_counter() - tc + n_stores += 1 + + # Batch-apply all collected pixel writes via numpy fancy indexing: + # one vectorised normalisation + three indexed writes per func, + # instead of three scalar __setitem__ calls per pixel. + tp = _time.perf_counter() + for px_list, py_list, val_list, item in pending.values(): + if not px_list: + continue + xs = np.asarray(px_list, dtype=np.intp) + ys = np.asarray(py_list, dtype=np.intp) + vals = np.asarray(val_list) + + min_v = item.config.min_value + max_v = item.config.max_value + if max_v > min_v: + normalized = np.clip( + (255.0 * (vals - min_v) / (max_v - min_v)), 0, 255 + ).astype(np.uint8) else: - x = y = 0 - - if lane < len(values): - td = _time.perf_counter() - item.update_pixel(x, y, values[lane], is_store=True) - self._prof_update_pixel += _time.perf_counter() - td - - self._prof_coord += _time.perf_counter() - tc + normalized = np.full(len(xs), 128, dtype=np.uint8) + + mask = (xs >= 0) & (xs < item.width) & (ys >= 0) & (ys < item.height) + xs = xs[mask] + ys = ys[mask] + normalized = normalized[mask] + if len(xs): + item.data[ys, xs, 0] = normalized + item.data[ys, xs, 1] = normalized + item.data[ys, xs, 2] = normalized + item._dirty = True + self._prof_update_pixel += _time.perf_counter() - tp + return n_stores def _get_func_item_for_packet(self, func_name: str) -> FuncItem | None: """Get the FuncItem for a packet's func name, with caching.""" @@ -937,8 +996,17 @@ def _get_func_item_for_packet(self, func_name: str) -> FuncItem | None: return None def _on_time_changed(self, time: int): - """Handle timeline scrubbing.""" - self._render_to_time(time) + """ + Handle timeline scrubbing — debounced so only the settled position renders. + """ + self._pending_scrub_time = time + self._scrub_timer.start() # restart resets the 50ms window + + def _on_scrub_settled(self): + """ + Called 50ms after the last slider movement; renders the final position. + """ + self._render_to_time(self._pending_scrub_time) def _on_play_toggled(self, playing: bool): """Handle play/pause toggle.""" diff --git a/python_bindings/src/halide/halide_/PyTrace.cpp b/python_bindings/src/halide/halide_/PyTrace.cpp index 4cf397d180ea..300a2b7c6fb9 100644 --- a/python_bindings/src/halide/halide_/PyTrace.cpp +++ b/python_bindings/src/halide/halide_/PyTrace.cpp @@ -319,6 +319,19 @@ class Trace { return result; } + // Returns the indices of all store packets, in order. + // Cached once at load time in Python to avoid iterating all packets per render. + std::vector store_indices() const { + std::vector result; + result.reserve(packets_.size() / 4); + for (size_t i = 0; i < packets_.size(); ++i) { + if (packets_[i].is_store()) { + result.push_back(i); + } + } + return result; + } + std::string dag_as_dot() const { std::ostringstream ss; ss << "digraph dag {\n"; @@ -533,6 +546,7 @@ void define_trace(py::module &m) { .def_property_readonly("dag_edges", &Trace::dag_edges) .def_property_readonly("packets", &Trace::packets) .def("filter_loads_stores", &Trace::filter_loads_stores) + .def("store_indices", &Trace::store_indices) .def("dag_as_dot", &Trace::dag_as_dot); } From 909c4edbc4583fbf1796a3ad2058d5094f813967 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 2 Jun 2026 19:46:24 -0700 Subject: [PATCH 07/67] feat: Initial commit for basic client-server setup for Halidoscope. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/README.md | 48 + apps/halidoscope/backend/backend/__init__.py | 0 apps/halidoscope/backend/backend/main.py | 264 + apps/halidoscope/backend/pyproject.toml | 28 + apps/halidoscope/backend/uv.lock | 1187 ++++ apps/halidoscope/frontend/.gitignore | 27 + apps/halidoscope/frontend/README.md | 11 + apps/halidoscope/frontend/index.html | 14 + apps/halidoscope/frontend/package.json | 29 + apps/halidoscope/frontend/pnpm-lock.yaml | 1625 ++++++ apps/halidoscope/frontend/pnpm-workspace.yaml | 2 + .../halidoscope/frontend/src-tauri/.gitignore | 7 + .../halidoscope/frontend/src-tauri/Cargo.lock | 5199 +++++++++++++++++ .../halidoscope/frontend/src-tauri/Cargo.toml | 27 + apps/halidoscope/frontend/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 13 + .../frontend/src-tauri/icons/128x128.png | Bin 0 -> 3512 bytes .../frontend/src-tauri/icons/128x128@2x.png | Bin 0 -> 7012 bytes .../frontend/src-tauri/icons/32x32.png | Bin 0 -> 974 bytes .../src-tauri/icons/Square107x107Logo.png | Bin 0 -> 2863 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 0 -> 3858 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 0 -> 3966 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 0 -> 7737 bytes .../src-tauri/icons/Square30x30Logo.png | Bin 0 -> 903 bytes .../src-tauri/icons/Square310x310Logo.png | Bin 0 -> 8591 bytes .../src-tauri/icons/Square44x44Logo.png | Bin 0 -> 1299 bytes .../src-tauri/icons/Square71x71Logo.png | Bin 0 -> 2011 bytes .../src-tauri/icons/Square89x89Logo.png | Bin 0 -> 2468 bytes .../frontend/src-tauri/icons/StoreLogo.png | Bin 0 -> 1523 bytes .../frontend/src-tauri/icons/icon.icns | Bin 0 -> 98451 bytes .../frontend/src-tauri/icons/icon.ico | Bin 0 -> 86642 bytes .../frontend/src-tauri/icons/icon.png | Bin 0 -> 14183 bytes .../halidoscope/frontend/src-tauri/src/lib.rs | 25 + .../frontend/src-tauri/src/main.rs | 6 + .../frontend/src-tauri/tauri.conf.json | 47 + apps/halidoscope/frontend/src/App.css | 17 + apps/halidoscope/frontend/src/App.tsx | 128 + .../frontend/src/components/FuncCanvas.tsx | 66 + .../frontend/src/components/Sidebar.tsx | 22 + apps/halidoscope/frontend/src/main.tsx | 9 + apps/halidoscope/frontend/src/types/index.ts | 7 + .../frontend/src/utils/constants.ts | 2 + apps/halidoscope/frontend/src/vite-env.d.ts | 1 + apps/halidoscope/frontend/tsconfig.json | 31 + apps/halidoscope/frontend/tsconfig.node.json | 12 + apps/halidoscope/frontend/vite.config.ts | 33 + 46 files changed, 8890 insertions(+) create mode 100644 apps/halidoscope/README.md create mode 100644 apps/halidoscope/backend/backend/__init__.py create mode 100644 apps/halidoscope/backend/backend/main.py create mode 100644 apps/halidoscope/backend/pyproject.toml create mode 100644 apps/halidoscope/backend/uv.lock create mode 100644 apps/halidoscope/frontend/.gitignore create mode 100644 apps/halidoscope/frontend/README.md create mode 100644 apps/halidoscope/frontend/index.html create mode 100644 apps/halidoscope/frontend/package.json create mode 100644 apps/halidoscope/frontend/pnpm-lock.yaml create mode 100644 apps/halidoscope/frontend/pnpm-workspace.yaml create mode 100644 apps/halidoscope/frontend/src-tauri/.gitignore create mode 100644 apps/halidoscope/frontend/src-tauri/Cargo.lock create mode 100644 apps/halidoscope/frontend/src-tauri/Cargo.toml create mode 100644 apps/halidoscope/frontend/src-tauri/build.rs create mode 100644 apps/halidoscope/frontend/src-tauri/capabilities/default.json create mode 100644 apps/halidoscope/frontend/src-tauri/icons/128x128.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/128x128@2x.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/32x32.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/Square107x107Logo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/Square142x142Logo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/Square150x150Logo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/Square284x284Logo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/Square30x30Logo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/Square310x310Logo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/Square44x44Logo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/Square71x71Logo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/Square89x89Logo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/StoreLogo.png create mode 100644 apps/halidoscope/frontend/src-tauri/icons/icon.icns create mode 100644 apps/halidoscope/frontend/src-tauri/icons/icon.ico create mode 100644 apps/halidoscope/frontend/src-tauri/icons/icon.png create mode 100644 apps/halidoscope/frontend/src-tauri/src/lib.rs create mode 100644 apps/halidoscope/frontend/src-tauri/src/main.rs create mode 100644 apps/halidoscope/frontend/src-tauri/tauri.conf.json create mode 100644 apps/halidoscope/frontend/src/App.css create mode 100644 apps/halidoscope/frontend/src/App.tsx create mode 100644 apps/halidoscope/frontend/src/components/FuncCanvas.tsx create mode 100644 apps/halidoscope/frontend/src/components/Sidebar.tsx create mode 100644 apps/halidoscope/frontend/src/main.tsx create mode 100644 apps/halidoscope/frontend/src/types/index.ts create mode 100644 apps/halidoscope/frontend/src/utils/constants.ts create mode 100644 apps/halidoscope/frontend/src/vite-env.d.ts create mode 100644 apps/halidoscope/frontend/tsconfig.json create mode 100644 apps/halidoscope/frontend/tsconfig.node.json create mode 100644 apps/halidoscope/frontend/vite.config.ts diff --git a/apps/halidoscope/README.md b/apps/halidoscope/README.md new file mode 100644 index 000000000000..160f3832ff70 --- /dev/null +++ b/apps/halidoscope/README.md @@ -0,0 +1,48 @@ +# Halidoscope + +(Another) Interactive trace visualizer for Halide. + +## Prerequisites + +You'll need a few prerequisites (in addition to the usual Halide development +setup) to get everything working. + +1. A [Rust](https://rust-lang.org/learn/get-started/) installation. +2. A [Node.js](https://nodejs.org/en/download) installation. +3. [PNPM](https://pnpm.io/), a space-efficient package manager for the + JavaScript ecosystem. + +> You can likely get away with using NPM directly, but `npm install` will not +> respect the version ranges in `pnpm-lock.yaml`. + +## Development + +### Backend + +```sh +uv sync --no-install-project halide +``` + +### Frontend + +```sh +pnpm install +``` + +## Starting Things Up + +1. Run the backend locally. + +```sh +cd backend +uv run dev +``` + +2. Run the frontend, pointing it at a Halide trace. + +```sh +cd frontend +pnpm tauri dev -- -- --trace +``` + +This should launch Halidoscope in development mode. diff --git a/apps/halidoscope/backend/backend/__init__.py b/apps/halidoscope/backend/backend/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/apps/halidoscope/backend/backend/main.py b/apps/halidoscope/backend/backend/main.py new file mode 100644 index 000000000000..daf5dedab3e1 --- /dev/null +++ b/apps/halidoscope/backend/backend/main.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +import bisect +import logging +import time as _time +import uuid +from typing import Any + +import numpy as np +import uvicorn +from fastapi import FastAPI, HTTPException, UploadFile +from pydantic import BaseModel +from fastapi.middleware.cors import CORSMiddleware +from fastapi.websockets import WebSocket, WebSocketDisconnect +from halide import FuncStats, Trace + +log = logging.getLogger(__name__) + +app = FastAPI(title="halide-viz backend") + +origins = ["http://localhost:1420"] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +_sessions: dict[str, Any] = {} +_packets: dict[str, Any] = {} +_store_indices: dict[str, list[int]] = {} +_func_name_cache: dict[ + str, dict[str, Any | None] +] = {} # session_id -> {packet_func -> stats | None} + + +def _serialize_func_stats(stats: FuncStats) -> dict[str, Any]: + return { + "name": stats.name, + "min_coords": list(stats.min_coords), + "max_coords": list(stats.max_coords), + "min_value": stats.min_value, + "max_value": stats.max_value, + } + + +def _register_trace(trace: Any) -> dict[str, Any]: + session_id = str(uuid.uuid4()) + payload = { + "session_id": session_id, + "funcs": {name: _serialize_func_stats(s) for name, s in trace.funcs.items()}, + "dag_edges": {k: list(v) for k, v in trace.dag_edges.items()}, + "pipelines": {str(k): v for k, v in trace.pipelines.items()}, + } + _sessions[session_id] = payload + # Cache once; avoids full C++ vector copy each access in render_ws. + _packets[session_id] = trace.packets + _store_indices[session_id] = list(trace.store_indices()) + _func_name_cache[session_id] = {} + return payload + + +@app.post("/load") +async def load_trace(file: UploadFile) -> dict[str, Any]: + data = await file.read() + trace = Trace.load_bytes(bytes(data)) + return _register_trace(trace) + + +class LoadPathRequest(BaseModel): + path: str + + +@app.post("/load-path") +async def load_trace_path(request: LoadPathRequest) -> dict[str, Any]: + try: + with open(request.path, "rb") as f: + data = f.read() + except OSError as e: + raise HTTPException(status_code=400, detail=str(e)) + trace = Trace.load_bytes(data) + return _register_trace(trace) + + +def _get_func_item_for_packet(session_id: str, func_name: str) -> Any: + cache = _func_name_cache[session_id] + if func_name in cache: + return cache[func_name] + + funcs = _sessions[session_id]["funcs"] + for name, stats in funcs.items(): + if func_name in name or name.endswith(f":{func_name}"): + cache[func_name] = stats + return stats + + cache[func_name] = None + return None + + +def _render_range(session_id: str, start: int, end: int) -> list[dict[str, Any]]: + packets = _packets[session_id] + store_indices = _store_indices[session_id] + + # pending: func_name -> [px_list, py_list, val_list, func_stats] + pending: dict[str, list] = {} + + end = min(end, len(packets)) + + lo = bisect.bisect_left(store_indices, start) + hi = bisect.bisect_right(store_indices, end - 1) + for si in range(lo, hi): + i = store_indices[si] + packet = packets[i] + + func_stats = _get_func_item_for_packet(session_id, packet.func) + if func_stats is None: + continue + + values = packet.get_values() + if not values: + continue + + coords = packet.coordinates + n_lanes = packet.type_lanes + dims_per_lane = len(coords) // n_lanes if n_lanes > 0 else len(coords) + min_coords = func_stats["min_coords"] + min_x = min_coords[0] if min_coords else 0 + min_y = min_coords[1] if len(min_coords) > 1 else 0 + + if packet.func not in pending: + pending[packet.func] = [[], [], [], func_stats] + px_list, py_list, val_list, _ = pending[packet.func] + + for lane in range(n_lanes): + if dims_per_lane >= 2: + x = coords[lane] - min_x + y = coords[n_lanes + lane] - min_y + elif dims_per_lane == 1: + x = coords[lane] - min_x + y = -min_y + else: + x = -min_x + y = -min_y + if lane < len(values): + px_list.append(x) + py_list.append(y) + val_list.append(values[lane]) + + updates = [] + for func_name, (px_list, py_list, val_list, func_stats) in pending.items(): + if not px_list: + continue + + xs = np.asarray(px_list, dtype=np.intp) + ys = np.asarray(py_list, dtype=np.intp) + vals = np.asarray(val_list) + + min_v = func_stats["min_value"] or 0.0 + max_v = func_stats["max_value"] or 255.0 + if max_v > min_v: + normalized = np.clip( + (255.0 * (vals - min_v) / (max_v - min_v)), 0, 255 + ).astype(np.uint8) + else: + normalized = np.full(len(xs), 128, dtype=np.uint8) + + min_coords = func_stats["min_coords"] + max_coords = func_stats["max_coords"] + width = ( + max(1, max_coords[0] - min_coords[0]) if min_coords and max_coords else 1 + ) + height = ( + max(1, max_coords[1] - min_coords[1]) + if len(min_coords) > 1 and len(max_coords) > 1 + else 1 + ) + + mask = (xs >= 0) & (xs < width) & (ys >= 0) & (ys < height) + xs = xs[mask] + ys = ys[mask] + normalized = normalized[mask] + + if len(xs): + updates.append( + { + "func": func_name, + "xs": xs.tolist(), + "ys": ys.tolist(), + "values": normalized.tolist(), + } + ) + + return updates + + +@app.websocket("/ws/{session_id}") +async def render_ws(websocket: WebSocket, session_id: str) -> None: + await websocket.accept() + + try: + if session_id not in _sessions: + await websocket.close(code=4004, reason="session not found") + return + + log.info("ws connected: session=%s", session_id) + + while True: + msg = await websocket.receive_json() + start: int = msg["start"] + end: int = msg["end"] + log.info("ws range request: start=%d end=%d", start, end) + + t0 = _time.perf_counter() + updates = _render_range(session_id, start, end) + t1 = _time.perf_counter() + + await websocket.send_json( + {"updates": updates, "done": True, "start": start, "end": end} + ) + t2 = _time.perf_counter() + + log.info( + "render=%dms send=%dms total=%dms funcs=%d start=%d end=%d", + 1000 * (t1 - t0), + 1000 * (t2 - t1), + 1000 * (t2 - t0), + len(updates), + start, + end, + ) + + except WebSocketDisconnect: + pass + except Exception: + log.exception("WebSocket error for session %s", session_id) + await websocket.close(code=1011, reason="internal error") + + +@app.get("/funcs/{session_id}") +async def get_funcs(session_id: str) -> dict[str, Any]: + if session_id not in _sessions: + raise HTTPException(status_code=404, detail="session not found") + trace = _sessions[session_id] + return {name: _serialize_func_stats(s) for name, s in trace.funcs.items()} + + +@app.delete("/session/{session_id}") +async def delete_session(session_id: str) -> dict[str, str]: + if session_id not in _sessions: + raise HTTPException(status_code=404, detail="session not found") + del _sessions[session_id] + del _packets[session_id] + del _store_indices[session_id] + del _func_name_cache[session_id] + + return {"deleted": session_id} + + +def run() -> None: + logging.basicConfig(level=logging.INFO) + uvicorn.run( + "backend.main:app", host="127.0.0.1", port=8765, reload=False, log_level="info" + ) diff --git a/apps/halidoscope/backend/pyproject.toml b/apps/halidoscope/backend/pyproject.toml new file mode 100644 index 000000000000..4907fce8dcf1 --- /dev/null +++ b/apps/halidoscope/backend/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "halidoscope" +version = "0.1.0" +description = "FastAPI backend for Halidoscope" +requires-python = ">=3.13" +dependencies = ["fastapi[standard]>=0.115", "halide", "numpy>=2.4.5"] + +[project.scripts] +dev = "backend.main:run" + +[tool.uv.sources] +halide = { path = "../../.." } +numpy = { index = "piwheels", marker = "platform_machine == 'armv8l' or platform_machine == 'armv7l'" } + +[[tool.uv.index]] +name = "piwheels" +url = "https://piwheels.org/simple" +explicit = true + +[tool.hatch.build.targets.wheel] +packages = ["backend"] + +[dependency-groups] +dev = ["ruff>=0.14"] diff --git a/apps/halidoscope/backend/uv.lock b/apps/halidoscope/backend/uv.lock new file mode 100644 index 000000000000..fbb2e7acca75 --- /dev/null +++ b/apps/halidoscope/backend/uv.lock @@ -0,0 +1,1187 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" +resolution-markers = [ + "(python_full_version >= '3.14' and platform_machine == 'armv7l') or (python_full_version >= '3.14' and platform_machine == 'armv8l')", + "(python_full_version < '3.14' and platform_machine == 'armv7l') or (python_full_version < '3.14' and platform_machine == 'armv8l')", + "python_full_version >= '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", + "python_full_version < '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "detect-installer" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "fastar" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "detect-installer" }, + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/7c/f194925af8fabdb0b7a886a1b89087c0b7f327f99e79497a882aa94c1e34/fastapi_cloud_cli-0.19.0.tar.gz", hash = "sha256:f97b31c2ad6af3832eb4065870bdca3365b6e827a0ccf6eeb15e477bc1662b13", size = 57476, upload-time = "2026-06-01T08:24:03.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/e6/1a2ec890fc273b9da2b173ca45f692a2e24a369bdd39ea7812c1d8a799e5/fastapi_cloud_cli-0.19.0-py3-none-any.whl", hash = "sha256:a2dfc4074c321e63ec88589cc1f90573d4b5bf980ddc44a7033e6f3cd8e96628", size = 38239, upload-time = "2026-06-01T08:24:02.437Z" }, +] + +[[package]] +name = "fastar" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/d6/3be260037e86fb694e88d47f583bac3a0188c99cee1a6b257ac26cb6b53c/fastar-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:33f544b08b4541b678e53749b4552a44720d96761fb79c172b005b1089c443ed", size = 707975, upload-time = "2026-04-13T17:09:58.866Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/d11d84bdd5e0e377771b955755771e3460b290da5809cb78c1b735ee2228/fastar-0.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:881247e6b6eaea59fc6569f9b61447aa6b9fc2ee864e048b4643d69c52745805", size = 863054, upload-time = "2026-04-13T17:09:13.048Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9b/fa42ea1188b144bac4b1b60753dfd449974a4d5eda132029ee7711569f94/fastar-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e8b993cb5613bab495ed482810bedc0986633fcb9a3b55c37ec88e0d6714f6a", size = 1071147, upload-time = "2026-04-13T17:10:48.833Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, + { url = "https://files.pythonhosted.org/packages/db/33/5f11f23eca0a569cd052507bc45dda2e5468697f8665728d25be44120f7d/fastar-0.11.0-cp313-cp313-win32.whl", hash = "sha256:c5f63d4d99ff4bfb37c659982ec413358bdee747005348756cc50a04d412d989", size = 454089, upload-time = "2026-04-13T17:11:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/35ff03c939cba7a255a9132367873fec6c355fd06a7f84fedcbaf4c8129f/fastar-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8690ed1928d31ded3ada308e1086525fb3871f5fa81e1b69601a3f7774004583", size = 486312, upload-time = "2026-04-13T17:11:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/ee9246cbfcbfd4144558f35e7e9a306ffe0a7564730a5188c45f21d2dab8/fastar-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:d977ded9d98a0719a305e0a4d5ee811f1d3e856d853a50acb8ae833c3cd6d5d2", size = 461975, upload-time = "2026-04-13T17:11:22.589Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cd/3644c48ecac456f928c12d47ec3bed36c36555b17c3859856f1ff860265d/fastar-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:71375bd6f03c2a43eb47bd949ea38ff45434917f9cdac79675c5b9f60de4fa73", size = 707860, upload-time = "2026-04-13T17:10:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/dee04476ae3626b2b040a60ad84628f77e1ffd8444232f2426b0ca1e0d7e/fastar-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eddfd9cab16e19ae247fe44bf992cb403ccfe27d3931d6de29a4695d95ad386c", size = 628216, upload-time = "2026-04-13T17:09:45.355Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5e/9395c7353d079cb4f5be0f7982ce0dc9f2e7dec5fd175eef466729d6023a/fastar-0.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c371f1d4386c699018bb64eb2fa785feacf32785559049d2bb72fe4af023f53", size = 864378, upload-time = "2026-04-13T17:09:14.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/1e4f67148223ff219612b6281a6000357abbcc2417964fa5c83f11d68fce/fastar-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cad7fa41e3e66554387481c1a09365e4638becd322904932674159d5f4046728", size = 760921, upload-time = "2026-04-13T17:07:59.138Z" }, + { url = "https://files.pythonhosted.org/packages/0f/82/09d11fb6d12f17993ffaf32ffd30c3c121a11e2966e84f19fb6f66430118/fastar-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf36652fa71b83761717c9899b98732498f8a2cb6327ff16bbf07f6be85c3437", size = 757012, upload-time = "2026-04-13T17:08:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/52/1f/5aeeacc4cb65615e2c9292cd9c5b0cd6fb6d2e6ee472ca6adc6c1b1b22ef/fastar-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f68ff8c17833053da4841720e95edde80ce45bb994b6b7d51418dddaac70ee47", size = 924510, upload-time = "2026-04-13T17:08:28.741Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1a/1e5bdabbeaf2e856928956292609f2ff6a650f94480fb8afaca30229e483/fastar-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4563ed37a12ea1cdc398af8571258d24b988bf342b7b3bf5451bd5891243280c", size = 816602, upload-time = "2026-04-13T17:08:59.461Z" }, + { url = "https://files.pythonhosted.org/packages/87/24/f960147910da3bed41a3adfcb026e17d5f50f4cf467a3324237a7088f61a/fastar-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee63c9875cba3b70dc44338c560facc5d6e763047dcc4a30501f9a68cf5f890", size = 819452, upload-time = "2026-04-13T17:09:29.926Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f4/3e77d7901d5707fd7f8a352e153c8ae09ea974e6fabad0b7c4eb9944b8d4/fastar-0.11.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:bd76bfffae6d0a91f4ac4a612f721e7aec108db97dccdd120ae063cd66959f27", size = 885254, upload-time = "2026-04-13T17:08:44.285Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/1585edd5ec47782ae93cd94edf05828e0ab02ef00aec00aea4194a600464/fastar-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5b707501ec01c1bc0518f741f01d322e50c9adc19a451aa24f67a2316e9397", size = 971496, upload-time = "2026-04-13T17:10:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e9/6874c9d1236ded565a0bed54b320ac9f165f287b1d89490fb70f9f323c81/fastar-0.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:37c0b5a88a657839aad98b0a6c9e4ac4c2c15d6b49c44ee3935c6b08e9d3e479", size = 1034685, upload-time = "2026-04-13T17:10:34.063Z" }, + { url = "https://files.pythonhosted.org/packages/14/d8/4ab20613ce2983427aee958e39be878dba874aa227c530a845e32429c4f6/fastar-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6c55f536c62a6efb180c1af0d5182948bff576bbfe6276e8e1359c9c7d2215d8", size = 1072675, upload-time = "2026-04-13T17:10:50.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/5ac3b7c20ce4b08f011dd2b979f96caabe64f9b10b157f211ea91bdfadca/fastar-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3082eeca59e189b9039335862f4c2780c0c8871d656bfdf559db4414a105b251", size = 1029330, upload-time = "2026-04-13T17:11:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/37cd6a1d4e288292170b64e19d79ecce2a7de8bb76790323399a2abc4619/fastar-0.11.0-cp314-cp314-win32.whl", hash = "sha256:b201a0a4e29f9fec2a177e13154b8725ec65ab9f83bd6415483efaa2aa18344b", size = 453940, upload-time = "2026-04-13T17:11:48.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1c/795c878b1ee29d79021cf8ed81f18f2b25ccde58453b0d34b9bdc7e025ea/fastar-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:868fddb26072a43e870a8819134b9f80ee602931be5a76e6fb873e04da343637", size = 486334, upload-time = "2026-04-13T17:11:34.882Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a4/113f104301df8bddcc0b3775b611a30cb7610baa3add933c7ccac9386467/fastar-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:3db39c9cc42abb0c780a26b299f24dfbc8be455985e969e15336d70d7b2f833b", size = 461534, upload-time = "2026-04-13T17:11:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/5c5f2c2c8e0c63e56a5636ebc7721589c889e94c0092cec7eb28ae7207e6/fastar-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:49c3299dec5e125e7ebaa27545714da9c7391777366015427e0ae62d548b442b", size = 707156, upload-time = "2026-04-13T17:10:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/df/f7/982c01b61f0fc135ad2b16d01e6d0ee53cf8791e68827f5f7c5a65b2e5b1/fastar-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3328ed1ed56d31f5198350b17dd60449b8d6b9d47abb4688bab6aef4450a165b", size = 627032, upload-time = "2026-04-13T17:09:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c3/38f1dac77ae0c71c37b176277c96d830796b8ce2fe69705f917829b53829/fastar-0.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd3eca3bbfec84a614bcb4143b4ad4f784d0895babc26cfc88436af88ca23c7a", size = 864403, upload-time = "2026-04-13T17:09:16.58Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/e69c363bdb3e5a5848e937b662b5469581ee6682c51bc1c0556494773929/fastar-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff86a967acb0d621dd24063dda090daa67bf4993b9570e97fe156de88a9006ca", size = 759480, upload-time = "2026-04-13T17:08:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/3b/29/4d8737590c2a6357d614d7cc7288e8f68e7e449680b8922997cc4349e65e/fastar-0.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86eaf7c0e985d93a7734168be2fb232b2a8cca53e41431c2782d7c12b12c03b1", size = 756219, upload-time = "2026-04-13T17:08:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ec/400de7b3b7d48801908f19cf5462177104395799472671b3e8152b2b04ca/fastar-0.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91f07b0b8eb67e2f177733a1f884edad7dfb9f8977ffef15927b20cb9604027d", size = 923669, upload-time = "2026-04-13T17:08:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/01/8926c53da923fed7ab4b96e7fbf7f73b663beb4f02095b654d6fab46f9ad/fastar-0.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f85c896885eb4abf1a635d54dea22cac6ae48d04fc2ea26ae652fcf1febe1220", size = 815729, upload-time = "2026-04-13T17:09:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/89/f0/5fef4c7946e352651b504b1a4235dac3505e7cfd24020788ab50552e84bf/fastar-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:075c07095c8de4b774ba8f28b9c0a02b1a2cd254da50cbe464dd3bb2432e9158", size = 819812, upload-time = "2026-04-13T17:09:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c8/0ebc3298b4a45e7bddc50b169ae6a6f5b80c939394d4befe6e60de535ee7/fastar-0.11.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:07f028933820c65750baf3383b807ecce1cd9385cf00ce192b79d263ad6b856c", size = 884074, upload-time = "2026-04-13T17:08:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/7baa4cdff8d6fbca41fa5c764b48a941fed8a9ec6c4cc92de65895a28299/fastar-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:039f875efa0f01fa43c20bf4e2fc7305489c61d0ac76eda991acfba7820a0e63", size = 969450, upload-time = "2026-04-13T17:10:18.667Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dc/1ebbfb58a47056ba866494f19efbcdd2ba2897096b94f36e796594b4d05b/fastar-0.11.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fff12452a9a5c6814a012445f26365541cc3d99dcca61f09762e6a389f7a32ea", size = 1033775, upload-time = "2026-04-13T17:10:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/ce4e3914066f08c99eb8c32952cc07c1a013e81b1db1b0f598130bf6b974/fastar-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2bf733e09f942b6fa876efe30a90508d1f4caef5630c00fb2a84fba355873712", size = 1072158, upload-time = "2026-04-13T17:10:52.497Z" }, + { url = "https://files.pythonhosted.org/packages/03/2a/6bca72992c84151c387cc6558f3867f5ebe5fb3684ee6fa9b76280ba4b8e/fastar-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d1531fa848fdd3677d2dce0a4b436ea64d9ae38fb8babe2ddbc180dd153cb7a3", size = 1028577, upload-time = "2026-04-13T17:11:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/83/18/7a7c15657a3da5569b26fc51cde6a80f8d84cb54b3b1aea6d74a103db4ad/fastar-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:5744551bc67c6fc6581cbd0e34a0fd6e2cd0bd30b43e94b1c3119cf35064b162", size = 453601, upload-time = "2026-04-13T17:11:53.726Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/331b59a6de279f3ad75c10c02c40a12f21d64a437d9c3d6f1af2dcbd7a76/fastar-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f4ce44e3b56c47cf38244b98d29f269b259740a580c47a2552efa5b96a5458fb", size = 486436, upload-time = "2026-04-13T17:11:40.089Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fd/5390ec4f49100f3ecb9968a392f9e6d039f1e3fe0ecd28443716ff01e589/fastar-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:76c1359314355eafbc6989f20fb1ad565a3d10200117923b9da765a17e2f6f11", size = 461049, upload-time = "2026-04-13T17:11:25.918Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "halide" +source = { directory = "../../../" } +dependencies = [ + { name = "imageio", version = "2.37.3", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, + { name = "imageio", version = "2.37.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "pillow", version = "12.2.0", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, +] + +[package.metadata] +requires-dist = [ + { name = "imageio", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=2" }, + { name = "imageio", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=2", index = "https://piwheels.org/simple" }, + { name = "numpy", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=1.26" }, + { name = "numpy", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=1.26", index = "https://piwheels.org/simple" }, + { name = "pillow", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", index = "https://piwheels.org/simple" }, +] + +[package.metadata.requires-dev] +apps = [ + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, +] +ci-base = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "pre-commit", specifier = ">=4" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, + { name = "tbump", specifier = ">=6.11" }, +] +ci-llvm-21 = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "halide-llvm", specifier = "~=21.1.0", index = "https://pypi.halide-lang.org/simple" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "pre-commit", specifier = ">=4" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, + { name = "tbump", specifier = ">=6.11" }, +] +ci-llvm-22 = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "halide-llvm", specifier = "~=22.1.0", index = "https://pypi.halide-lang.org/simple" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "pre-commit", specifier = ">=4" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, + { name = "tbump", specifier = ">=6.11" }, +] +ci-llvm-main = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "halide-llvm", specifier = "~=23.0.0.dev0", index = "https://pypi.halide-lang.org/simple" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, + { name = "pre-commit", specifier = ">=4" }, + { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, + { name = "tbump", specifier = ">=6.11" }, +] +dev = [ + { name = "pybind11", specifier = ">=2.11.1" }, + { name = "scikit-build-core", specifier = "~=0.11.0" }, + { name = "setuptools-scm", specifier = ">=8.3.1" }, +] +tools = [ + { name = "cmake", specifier = ">=3.28" }, + { name = "ninja", specifier = ">=1.11,!=1.13.0" }, + { name = "pre-commit", specifier = ">=4" }, + { name = "ruff", specifier = ">=0.12" }, + { name = "tbump", specifier = ">=6.11" }, +] + +[[package]] +name = "halidoscope" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "fastapi", extra = ["standard"] }, + { name = "halide" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", extras = ["standard"], specifier = ">=0.115" }, + { name = "halide", directory = "../../../" }, + { name = "numpy", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=2.4.5" }, + { name = "numpy", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=2.4.5", index = "https://piwheels.org/simple" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.14" }] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.3" +source = { registry = "https://piwheels.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.14' and platform_machine == 'armv7l') or (python_full_version >= '3.14' and platform_machine == 'armv8l')", + "(python_full_version < '3.14' and platform_machine == 'armv7l') or (python_full_version < '3.14' and platform_machine == 'armv8l')", +] +dependencies = [ + { name = "numpy", version = "2.4.5", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, + { name = "pillow", version = "12.2.0", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, +] +wheels = [ + { url = "https://piwheels.org/simple/imageio/imageio-2.37.3-py3-none-any.whl", hash = "sha256:06c1f430a489e305a69e006b6877451fdffe2c506099e9792d94fae3bb69cd7a" }, +] + +[[package]] +name = "imageio" +version = "2.37.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", + "python_full_version < '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", +] +dependencies = [ + { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, + { name = "pillow", version = "12.2.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.5" +source = { registry = "https://piwheels.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.14' and platform_machine == 'armv7l') or (python_full_version >= '3.14' and platform_machine == 'armv8l')", + "(python_full_version < '3.14' and platform_machine == 'armv7l') or (python_full_version < '3.14' and platform_machine == 'armv8l')", +] +wheels = [ + { url = "https://piwheels.org/simple/numpy/numpy-2.4.5-cp313-cp313-linux_armv7l.whl", hash = "sha256:b2fcc5c1c99207339ce3c69cab6a7714f8923d3953b4e892cb061610c36bf669" }, +] + +[[package]] +name = "numpy" +version = "2.4.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", + "python_full_version < '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", +] +sdist = { url = "https://files.pythonhosted.org/packages/50/8e/b8041bc719f056afd864478029d52214789341ac6583437b0ee5031e9530/numpy-2.4.5.tar.gz", hash = "sha256:ca670567a5683b7c1670ec03e0ddd5862e10934e92a70751d68d7b7b74ca7f9f", size = 20735669, upload-time = "2026-05-15T20:25:19.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/a4/fb50657c7cab297bf34edcd60a074cb0647f61771430d6363575274160fe/numpy-2.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ef248460b645c102026b82337cc4e88231909c66dd77b59ec6d6cac7e44f277", size = 16684760, upload-time = "2026-05-15T20:23:19.436Z" }, + { url = "https://files.pythonhosted.org/packages/3e/43/87e731299b9408eda705b3b9cb31c7bceb9347d2af9cbb16b2b1e4b5bc0f/numpy-2.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4603622bdcdbf8dccb1d9d5b21d16a7aa4e473ae6c8e14048d846fd4ca2907a0", size = 14694117, upload-time = "2026-05-15T20:23:21.832Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/0b2bb8acea222e9dd6e582afc2bc553b89b8833cbdccc68e68f050fb31f8/numpy-2.4.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6c18d49c67689c562854b53fdc433b93e47c12952aa6fa6d59f185e1a5992419", size = 5199141, upload-time = "2026-05-15T20:23:24.066Z" }, + { url = "https://files.pythonhosted.org/packages/39/60/b6972b5d47033d90000f0097c81a98b9486589a2d7003bf725bff275cb0d/numpy-2.4.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b1c663ddc641f4192e90511bec61a09bc231e3bbdb996cdc6edbcaa0e528d685", size = 6546954, upload-time = "2026-05-15T20:23:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e9/ed667cb12c11ca0adde431f685d3a5dd78e6f78b27228c581c8415198e9e/numpy-2.4.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93793222b524f692f12b2f8752ce8b1d9d9125b2bfd5dbf0fb69c92c5e1ce86c", size = 15669430, upload-time = "2026-05-15T20:23:28.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/e5/679f6ffeb01294b0008e5ada4a113cb47617bc0e1819a529fd7973c6d7f4/numpy-2.4.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1616bde34b2bcba2fa9bde06217ce00da4f3d1bdfb264d54525a99e8fe170d83", size = 16633390, upload-time = "2026-05-15T20:23:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/36/46/42bfffc9a780ec902ccd7470d3219192ee82b7b442710307dd85b4d121b0/numpy-2.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09d7d97da1c2c62f4818b3e150a57572ff8dcf1cf5ac501aac832ffd4ebd9566", size = 17020709, upload-time = "2026-05-15T20:23:34.08Z" }, + { url = "https://files.pythonhosted.org/packages/44/00/3e840bfee0cc6cec22209f2c97057f26eeb30de031e4933b4dfc0395416c/numpy-2.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d68d0b355ab2e39fe0de59001d7151dfdbbb880ef67baeed806661e03df5097", size = 18357818, upload-time = "2026-05-15T20:23:36.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/cb/3447b400b9da84134575486f0f656541559b00d4b262477bce9b678bbca8/numpy-2.4.5-cp313-cp313-win32.whl", hash = "sha256:fe28b64777ddfa0eca9b5f51474034ebe3dcb8324f48f27b28f479085673ae33", size = 5961114, upload-time = "2026-05-15T20:23:39.586Z" }, + { url = "https://files.pythonhosted.org/packages/28/f9/a90d2220ffcdc0798f5d55bb5d5463cd6254ec9ef43f384dae80217d7a2f/numpy-2.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:fb4a6c9c537d6ccec9cc4aeae4261bd3cc79b070c67ddc0646f5b1c07fddde42", size = 12318553, upload-time = "2026-05-15T20:23:41.436Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/96f531fb3234545315152d34efdf3de7daee81254448447eb619e8d16967/numpy-2.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:6d7df2da2e7ea0624a43aa368104b3a3ce14aae98ad4bb2c9a93fecef76f1c97", size = 10222200, upload-time = "2026-05-15T20:23:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/a291caab5a3c520babf93ff77c54fd5fdb1ebbc3296cee2eb2146ce773b1/numpy-2.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:2a235607a18df941760a695927051af4b1cd5d3ee85840d0e2af816785771feb", size = 14821438, upload-time = "2026-05-15T20:23:45.911Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/13dbb1159b864370568e7309063fd72667984df89db74e9caeb175d067c7/numpy-2.4.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:58dcf64969d870f36bc7fbd557d2617e997db7dc06261b6e3327148ea460d0a4", size = 5326663, upload-time = "2026-05-15T20:23:48.18Z" }, + { url = "https://files.pythonhosted.org/packages/7c/99/d233408072a0e019e2288e27edd23f7d572ccd4a73d1539baa3270ede85d/numpy-2.4.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:235f54b0156274d8fa3155db3ed6d2f401c7e8f3367c90db0a12f02a58fde6ed", size = 6646874, upload-time = "2026-05-15T20:23:49.856Z" }, + { url = "https://files.pythonhosted.org/packages/c5/00/eeb6f193dfe767725e952e0464f3e51f44145c5dd261cd7389aa36ac0713/numpy-2.4.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3b5bb65437a3555c648e706475db01c645559ca80dc8b03e4f202ea757e0d6", size = 15728147, upload-time = "2026-05-15T20:23:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c9/b8ed039f1fde1b13a8807c893e7e2f9432a379f4d6401edecf0028da5b2c/numpy-2.4.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f09a7e5f017d7098c66522097c96257411c9620c0926212200d66bc8cee3976", size = 16681770, upload-time = "2026-05-15T20:23:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/11/5b/0198ef6cb7016eca6d895d392106012138127fab23f46637e76d5e25c9f5/numpy-2.4.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:993a88d8fdd8554466a8765cd8bacd97ba56b70ca6b0a04bcdca77f5afed4222", size = 17086218, upload-time = "2026-05-15T20:23:56.646Z" }, + { url = "https://files.pythonhosted.org/packages/f0/fe/8821f3cfc660ae84c92ee158505941874b62c56a42e035a41425228cd8cf/numpy-2.4.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:84f58bed609b5669f5ad3d597901a4f1f86ee5b3c3708aaa55f05b4fe6e0f656", size = 18403542, upload-time = "2026-05-15T20:23:59.173Z" }, + { url = "https://files.pythonhosted.org/packages/0e/00/e64ecaf498865e7b091f57658b2c522503e5d1b70e43b807f5f8247e1d88/numpy-2.4.5-cp313-cp313t-win32.whl", hash = "sha256:7200c58f3f933ca61e66346667dcc8510bb111995e9ce15398a731e6a4afa4bb", size = 6084903, upload-time = "2026-05-15T20:24:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/20/c0/354997dedaf74e8311c2cf9a6027b476fd8d424cb92189cc0ae2b25f501c/numpy-2.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c26c71080d35db5002102f5d9ff614d45de02aa1f7802943e691e063e5ee93bc", size = 12458420, upload-time = "2026-05-15T20:24:03.735Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/917ee5ea4a31ca1a6e4c9a85386477efa318dcc60db257c5ef4adda096c1/numpy-2.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:2caa576d1707b275cba1aeb60a5c50daa6fa2a3f28ecb08123bc05fd439005db", size = 10291826, upload-time = "2026-05-15T20:24:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c1/3be0bf102fc17cff5bd142e3be0bfffabec6fa46da0a462396c76b0765d0/numpy-2.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:889ca2c072315de638a5194a772aa1fa2df92bdd6175f6a222d4784040424b61", size = 16683455, upload-time = "2026-05-15T20:24:08.988Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3e/0742d724901fa36bc54b338c6e62e463a7601180da896aa44978f0adf004/numpy-2.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89e89304fb1f8c3f0ecfa4a7d48f311dd79771336a940e920159d643d1307e77", size = 14704577, upload-time = "2026-05-15T20:24:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:144fcc5a3a17679b2b82543b4a2d8dd29937230a7af13232b5f753872feb6361", size = 5209756, upload-time = "2026-05-15T20:24:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/52/c0/23fb1bc506f774e03db66219a2830e720f4d3dbcaaddf855a7ff7bb6d96f/numpy-2.4.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:398bb16772b265b9fa5c07b07072646ea97137c10ffb62a9a087b277fc825c29", size = 6543937, upload-time = "2026-05-15T20:24:16.223Z" }, + { url = "https://files.pythonhosted.org/packages/9f/49/db4662c26e68520afcc84d672a6f9f5294063dee0e57a46d61afdaa7f9ed/numpy-2.4.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb352e7b8876da1249e72254736d6c58c505fa4e58a3d7e30efca241ca9ca9ce", size = 15685292, upload-time = "2026-05-15T20:24:17.978Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7341b08ff8124d7353939778e2707b8732d03c78c1c30e0815aba2dacbe1245a", size = 16638528, upload-time = "2026-05-15T20:24:20.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/81/364388600932618fe735d97fdd2437cb8dd87a23377ac11d8b9d5db098b7/numpy-2.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:deb01226f012539f3945261ffe1c10aec081a0fa0a5c925419933c70f3ae2d23", size = 17036709, upload-time = "2026-05-15T20:24:22.949Z" }, + { url = "https://files.pythonhosted.org/packages/32/4a/a1185b18a94a6d9587e54b437e7d0ba36ecf6e614f1bea03f5249912c64e/numpy-2.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d888bdf7335f76878c3c7b264ac1ff089863e211ec81249f9fb5795c2183dc25", size = 18363254, upload-time = "2026-05-15T20:24:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8e/95c1d2ed15ae97750ede8c8a0ac487c9c01207afff430f47078b1d9d7dc5/numpy-2.4.5-cp314-cp314-win32.whl", hash = "sha256:15f90d1256e9b2320aff24fde44815b787ab6d7c49a1a11bfd8138b321c5f080", size = 6010184, upload-time = "2026-05-15T20:24:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4bd2cd4ef9c0afa87de73723c0a33c0edff62143e1432917458e26d3d195d87f", size = 12450344, upload-time = "2026-05-15T20:24:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/3d/64/c0ae481f7c3b2f85869bcd8fc5d30aa7c96b394162eef9c9315957f115c5/numpy-2.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:db304568c650e9d7039744d3575d0d287754debb2057d7c7b8cdfdc2c487a957", size = 10495674, upload-time = "2026-05-15T20:24:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/57/89/c5a4c677acf17aa50ba09a15e61812f90baac42bb6ca38d112e005858351/numpy-2.4.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de2883e0d2c63eae1bab1a84b390dca74aabb3d20ea1f5d58f360853c83abf3", size = 14824078, upload-time = "2026-05-15T20:24:34.669Z" }, + { url = "https://files.pythonhosted.org/packages/e7/52/57e7144284f6b51ba93523e495ff239260b1ecd5257e3700a436332e5688/numpy-2.4.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:06760fe73ae5005008748d182de612c733542af3cde063d532cd2127561b27be", size = 5329246, upload-time = "2026-05-15T20:24:36.957Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b3/09dbce80fd4a7db4318f2fc01eec0ae76f29306442b5a32d4b811d082cdf/numpy-2.4.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:4b51a01745cb04cc19278482207444b4d30728ce91c28d27a3bfae5fc6ff24c7", size = 6649877, upload-time = "2026-05-15T20:24:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/dbdb23e82d540b757690ef13f011c386fca6a63848eec6136baf8ce7cbed/numpy-2.4.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a05636d7937d0936f271e5ba957fa8d746b5be3c2025caa1a2508f4fe521d40", size = 15730534, upload-time = "2026-05-15T20:24:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bd/68f6e9b3c20decf40ac06708a7b506757e3a8588efed32988d1b747316be/numpy-2.4.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b86f56048ed09c3bbe48962a7dff077c2fd3274f8cf981800f3b38eac49cc3", size = 16679741, upload-time = "2026-05-15T20:24:44.874Z" }, + { url = "https://files.pythonhosted.org/packages/39/1d/0fcac0b6b4ea1b50ca8fca05a34bed5c8d56e34c1cb5ffb04cf76109ac3c/numpy-2.4.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:130d58151c4db23e9fa860b84784e219a3aa3e030acc88a493ea37006c4dfd4c", size = 17085598, upload-time = "2026-05-15T20:24:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e8/a472b2564cf6cc498ad7aa9741d9832648221b8ab8cc0dbef41faa248ede/numpy-2.4.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d475afc8cbe935ff5944f753d863bba774d7f4e1feaaa4102901e3e053ca5963", size = 18403855, upload-time = "2026-05-15T20:24:50.474Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a4/da82196f8cc4bd28ecf17bd57008c84f3d4696caf06753d9bad45e4ad749/numpy-2.4.5-cp314-cp314t-win32.whl", hash = "sha256:27f4a6dc26353a860b348961b9aa9e009835688b435cfa105e873b8dc2c726f5", size = 6156900, upload-time = "2026-05-15T20:24:53.134Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/860959b91a73d9a085006554fa3850da51a7ffab64599bac5097243438ab/numpy-2.4.5-cp314-cp314t-win_amd64.whl", hash = "sha256:76ac6e90f5e226011c88f9b7040a4bcae612518bc7e9adc127e697a13b28ad1a", size = 12638906, upload-time = "2026-05-15T20:24:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2a/bbd3097913083ad07c0f28fc9629666221fc18923e17ce97ae22a5dccdd6/numpy-2.4.5-cp314-cp314t-win_arm64.whl", hash = "sha256:7c392e2c1bf596701d3c6832be7567eab5d5b0a13865036c33365ee097d37f8b", size = 10565875, upload-time = "2026-05-15T20:24:57.425Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://piwheels.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.14' and platform_machine == 'armv7l') or (python_full_version >= '3.14' and platform_machine == 'armv8l')", + "(python_full_version < '3.14' and platform_machine == 'armv7l') or (python_full_version < '3.14' and platform_machine == 'armv8l')", +] +wheels = [ + { url = "https://piwheels.org/simple/pillow/pillow-12.2.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:d4ff90fbce9831907b35a54405d8b1312c1b83e29f72cc45cf02e60ee55dd48b" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", + "python_full_version < '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/82/c8cd43a6e0719bf5a3b034f6726dd701f75829c08944c83d4b95d02ed0e8/python_multipart-0.0.30.tar.gz", hash = "sha256:0edfe0475c1f46ddd3ff7785a626f6118af32bdcf359bb21260367313bb32118", size = 46316, upload-time = "2026-05-31T19:24:55.198Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/fd/0318007beb234790993d3ec5afd051d1dbceb733e81e3afe2b981ece3f37/python_multipart-0.0.30-py3-none-any.whl", hash = "sha256:830964def8c90607ac5daa00514e3987815865713ade8d20febc9177ac0c3c5b", size = 29730, upload-time = "2026-05-31T19:24:53.814Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/49/d7a4fd4f39c195b73f78694af3e812943a4181a8d48a11035425d0f6d71f/rich_toolkit-0.20.0.tar.gz", hash = "sha256:bb05382554d4f46865dfca2fccccf30768ef37e0347207d00f034d9b36b25021", size = 203144, upload-time = "2026-06-02T21:11:38.48Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b5/6b6efd9e305653fae68ed0b712bc659cd3c5541ec54416e6bb14af52acca/rich_toolkit-0.20.0-py3-none-any.whl", hash = "sha256:906e5b8741fafc46159c5f719fd30fd3c9dd8f2c31b8161dc8c612f98b8da01a", size = 35379, upload-time = "2026-06-02T21:11:37.564Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.61.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "starlette" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/44/ec35f1b6e83094b997da438a02c8c9b0ade2b1e84cfc48bd4656780760a6/starlette-1.2.1.tar.gz", hash = "sha256:9b9b5ebb992e67d6093741e63c2f59e4f6fff986f81163c087867bd7b924b3f6", size = 2701854, upload-time = "2026-05-31T01:07:51.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" }, +] + +[[package]] +name = "typer" +version = "0.26.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/8dc5733b8939e7f1a71173091a6e27a1658345edbff548a0bf3f5bb26173/typer-0.26.6.tar.gz", hash = "sha256:cdbc160fe7e795b835fb6016419494a521a67bfb86b9476a1ccd0e7727d3ae5b", size = 201595, upload-time = "2026-06-02T13:47:50.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/c7/519138260db5e2fe03a509bf9e8ef6af9a514d3565c8fa74fc4fededbae1/typer-0.26.6-py3-none-any.whl", hash = "sha256:49f96d9ee5730cef607bbe155042f40b41fa4c0d0dec04990d580837493805be", size = 122464, upload-time = "2026-06-02T13:47:51.768Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] diff --git a/apps/halidoscope/frontend/.gitignore b/apps/halidoscope/frontend/.gitignore new file mode 100644 index 000000000000..ae6e31033ab7 --- /dev/null +++ b/apps/halidoscope/frontend/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Trace binaries +*.hltrace diff --git a/apps/halidoscope/frontend/README.md b/apps/halidoscope/frontend/README.md new file mode 100644 index 000000000000..3a142a74fdf3 --- /dev/null +++ b/apps/halidoscope/frontend/README.md @@ -0,0 +1,11 @@ +# Tauri + React + Typescript + +This template should help get you started developing with Tauri, React and +Typescript in Vite. + +## Recommended IDE Setup + +- [VS Code](https://code.visualstudio.com/) + + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + \+ + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/apps/halidoscope/frontend/index.html b/apps/halidoscope/frontend/index.html new file mode 100644 index 000000000000..ff93803bbc0a --- /dev/null +++ b/apps/halidoscope/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + Tauri + React + Typescript + + + +
+ + + diff --git a/apps/halidoscope/frontend/package.json b/apps/halidoscope/frontend/package.json new file mode 100644 index 000000000000..1c500766ab4c --- /dev/null +++ b/apps/halidoscope/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "halidoscope", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tailwindcss/vite": "^4.3.0", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-cli": "^2.4.1", + "@tauri-apps/plugin-opener": "^2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "typescript": "~5.8.3", + "vite": "^7.0.4" + } +} diff --git a/apps/halidoscope/frontend/pnpm-lock.yaml b/apps/halidoscope/frontend/pnpm-lock.yaml new file mode 100644 index 000000000000..a40d43231dee --- /dev/null +++ b/apps/halidoscope/frontend/pnpm-lock.yaml @@ -0,0 +1,1625 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0)) + '@tauri-apps/api': + specifier: ^2 + version: 2.11.0 + '@tauri-apps/plugin-cli': + specifier: ^2.4.1 + version: 2.4.1 + '@tauri-apps/plugin-opener': + specifier: ^2 + version: 2.5.4 + react: + specifier: ^19.1.0 + version: 19.2.6 + react-dom: + specifier: ^19.1.0 + version: 19.2.6(react@19.2.6) + tailwindcss: + specifier: ^4.3.0 + version: 4.3.0 + devDependencies: + '@tauri-apps/cli': + specifier: ^2 + version: 2.11.2 + '@types/react': + specifier: ^19.1.8 + version: 19.2.15 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.2.3(@types/react@19.2.15) + '@vitejs/plugin-react': + specifier: ^4.6.0 + version: 4.7.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0)) + typescript: + specifier: ~5.8.3 + version: 5.8.3 + vite: + specifier: ^7.0.4 + version: 7.3.5(jiti@2.7.0)(lightningcss@1.32.0) + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.61.0': + resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.0': + resolution: {integrity: sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.0': + resolution: {integrity: sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.0': + resolution: {integrity: sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.0': + resolution: {integrity: sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.0': + resolution: {integrity: sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.61.0': + resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.61.0': + resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.61.0': + resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.61.0': + resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.61.0': + resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.61.0': + resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.61.0': + resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.61.0': + resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.61.0': + resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.61.0': + resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.0': + resolution: {integrity: sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.0': + resolution: {integrity: sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.0': + resolution: {integrity: sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.0': + resolution: {integrity: sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.0': + resolution: {integrity: sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tauri-apps/api@2.11.0': + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} + + '@tauri-apps/cli-darwin-arm64@2.11.2': + resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.2': + resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.2': + resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.2': + resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-cli@2.4.1': + resolution: {integrity: sha512-8JXofQFI5cmiGolh1PlU4hzE2YJgrgB1lyaztyBYiiMCy13luVxBXaXChYPeqMkUo46J1UadxvYdjRjj0E8zaw==} + + '@tauri-apps/plugin-opener@2.5.4': + resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + rollup@4.61.0: + resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vite@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.61.0': + optional: true + + '@rollup/rollup-android-arm64@4.61.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.0': + optional: true + + '@rollup/rollup-darwin-x64@4.61.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.0': + optional: true + + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 7.3.5(jiti@2.7.0)(lightningcss@1.32.0) + + '@tauri-apps/api@2.11.0': {} + + '@tauri-apps/cli-darwin-arm64@2.11.2': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.2': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.2': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + optional: true + + '@tauri-apps/cli@2.11.2': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.2 + '@tauri-apps/cli-darwin-x64': 2.11.2 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.2 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.2 + '@tauri-apps/cli-linux-arm64-musl': 2.11.2 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-musl': 2.11.2 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.2 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 + '@tauri-apps/cli-win32-x64-msvc': 2.11.2 + + '@tauri-apps/plugin-cli@2.4.1': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-opener@2.5.4': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/estree@1.0.9': {} + + '@types/react-dom@19.2.3(@types/react@19.2.15)': + dependencies: + '@types/react': 19.2.15 + + '@types/react@19.2.15': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.3.5(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color + + baseline-browser-mapping@2.10.33: {} + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + caniuse-lite@1.0.30001793: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.364: {} + + enhanced-resolve@5.22.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + graceful-fs@4.2.11: {} + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + node-releases@2.0.46: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react-refresh@0.17.0: {} + + react@19.2.6: {} + + rollup@4.61.0: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.0 + '@rollup/rollup-android-arm64': 4.61.0 + '@rollup/rollup-darwin-arm64': 4.61.0 + '@rollup/rollup-darwin-x64': 4.61.0 + '@rollup/rollup-freebsd-arm64': 4.61.0 + '@rollup/rollup-freebsd-x64': 4.61.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.0 + '@rollup/rollup-linux-arm-musleabihf': 4.61.0 + '@rollup/rollup-linux-arm64-gnu': 4.61.0 + '@rollup/rollup-linux-arm64-musl': 4.61.0 + '@rollup/rollup-linux-loong64-gnu': 4.61.0 + '@rollup/rollup-linux-loong64-musl': 4.61.0 + '@rollup/rollup-linux-ppc64-gnu': 4.61.0 + '@rollup/rollup-linux-ppc64-musl': 4.61.0 + '@rollup/rollup-linux-riscv64-gnu': 4.61.0 + '@rollup/rollup-linux-riscv64-musl': 4.61.0 + '@rollup/rollup-linux-s390x-gnu': 4.61.0 + '@rollup/rollup-linux-x64-gnu': 4.61.0 + '@rollup/rollup-linux-x64-musl': 4.61.0 + '@rollup/rollup-openbsd-x64': 4.61.0 + '@rollup/rollup-openharmony-arm64': 4.61.0 + '@rollup/rollup-win32-arm64-msvc': 4.61.0 + '@rollup/rollup-win32-ia32-msvc': 4.61.0 + '@rollup/rollup-win32-x64-gnu': 4.61.0 + '@rollup/rollup-win32-x64-msvc': 4.61.0 + fsevents: 2.3.3 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + typescript@5.8.3: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.61.0 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + + yallist@3.1.1: {} diff --git a/apps/halidoscope/frontend/pnpm-workspace.yaml b/apps/halidoscope/frontend/pnpm-workspace.yaml new file mode 100644 index 000000000000..5ed0b5af0d45 --- /dev/null +++ b/apps/halidoscope/frontend/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true diff --git a/apps/halidoscope/frontend/src-tauri/.gitignore b/apps/halidoscope/frontend/src-tauri/.gitignore new file mode 100644 index 000000000000..b21bd681d997 --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/apps/halidoscope/frontend/src-tauri/Cargo.lock b/apps/halidoscope/frontend/src-tauri/Cargo.lock new file mode 100644 index 000000000000..1d477667488d --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/Cargo.lock @@ -0,0 +1,5199 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.12.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.12.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.12.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.12.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "frontend" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-cli", + "tauri-plugin-opener", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.12.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.12.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.12.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.12.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.12.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.12.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.12.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.12.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.12.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.12.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.12.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.12.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.12.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.12.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-cli" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28e78fb2c09a81546bcd376d34db4bda5769270d00990daa9f0d6e7ac1107e25" +dependencies = [ + "clap", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.12.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.12.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.12.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.3", +] diff --git a/apps/halidoscope/frontend/src-tauri/Cargo.toml b/apps/halidoscope/frontend/src-tauri/Cargo.toml new file mode 100644 index 000000000000..bfe44502d69b --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "frontend" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "frontend_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-opener = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +tauri-plugin-cli = "2.0.0" diff --git a/apps/halidoscope/frontend/src-tauri/build.rs b/apps/halidoscope/frontend/src-tauri/build.rs new file mode 100644 index 000000000000..d860e1e6a7ca --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/halidoscope/frontend/src-tauri/capabilities/default.json b/apps/halidoscope/frontend/src-tauri/capabilities/default.json new file mode 100644 index 000000000000..ceb4a9c62805 --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/capabilities/default.json @@ -0,0 +1,13 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "opener:default", + "cli:default" + ] +} diff --git a/apps/halidoscope/frontend/src-tauri/icons/128x128.png b/apps/halidoscope/frontend/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..6be5e50e9b9ae84d9e2ee433f32ef446495eaf3b GIT binary patch literal 3512 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 literal 0 HcmV?d00001 diff --git a/apps/halidoscope/frontend/src-tauri/icons/128x128@2x.png b/apps/halidoscope/frontend/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e81becee571e96f76aa5667f9324c05e5e7a4479 GIT binary patch literal 7012 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 Sh@|Ye~|0>}wMPLT8jm@Y!H33Sz}5aFI6 zM9Lzqz|;A*0sGs=2A1uU!1nk2dGF7knQwr99SAFen)x(eCO;F8y2C~0FD1YxRTPcy zPWVxkUYmeuz}Tv?7&Fe-!UE{)ZW)Mb;H)^#eHDv$`dkZGguJz@^MA!ZNGAUqt{|0H zpZ7Ch9S`q5!>R%}>}62!+(T^evyO+ImSo2wpu)su4^3nw5(%)KD%gbSev^*HZZ&3( z#&c@Z0gH|}Ck)w6fh0&NBJ62ib%R}(3@$VFl*_#l2W$wQ-~4RmZZAt5O*^2Q5}Xr8Hy@c`#pM?kc?hFWxRXr*mUfUCXf4ka5DD~ zat6d85COB05l#(P9*cQZ3EC8fVdS~?&vN#rce(aF9@xp80O2{{FBvU+{X>Hoh;xI` z{$e^Nw1y*VbO8wv`8|-m?NwNaKGTGaF{P^JLB^DbOYWIbn%eT`*!^C1H36=O8Z-M> zkD~88ry`eSo`tEBN4>w7OWZwUzlh{WM1m8R6zepqGcGMaV7vWY9b?K4b6~|HVG)ec wi>I@ws#sZo7or4_*4M>7;p5{nr2pZ?Uu4>Krr0kU)&Kwi07*qoM6N<$f)&@lf&c&j literal 0 HcmV?d00001 diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square107x107Logo.png b/apps/halidoscope/frontend/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0ca4f27198838968bd60ed7d371bfa23496b7fe5 GIT binary patch literal 2863 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 Y2S8W#vM)6=T>t<807*qoM6N<$f*y@n<^TWy literal 0 HcmV?d00001 diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square284x284Logo.png b/apps/halidoscope/frontend/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c021d2ba76619c08969ab688db3b27f29257aa6f GIT binary patch literal 7737 zcmb7Jg;N_$u*XVqcP+HI6emcbcyWR@NGVP!4k_-z3$#Gd;10#zDFKRmiUxN{p*TSv z-<$Ujyqnp%x!>;X&duEJ-R?%~XsHn5(cz(?p%JRSQ`AL6LudGpaIl{c%5(g+rwP~f z9moR>4WIl!LPyJh(ma9a9=a;>XjS73`%eojJ2_1`G_=|T{5y+hXlRV%s)};@-ss1O zAa@3(l;gYa~ymye90dKS59Fwku9(LU>G1vDh#kqqfKB7Ky8nVrYb&}|9_83 zEDbdDq08Q%sF5SpM;UYGcpN(X5X>Ssi)nBWC>OHArgc8Y|GrRNzQ0ymSIAu|h{8Tsam*AnS*~~*OqgM5)8If;hAL>=_Pfq`6uWNlV}|&e z6;n-2uztv`H7MezYVL|oZ&SS{?0&_`h*9#)bpEGK?-h=m2UXP&uh;eB2~X(s3s<_) zD|@oQw>Npx0ODf4=2>HMAhB;-uwLaxz+ z9S8buXpXtMMcddByd;pXQT5Vug+RR==Y}mg>hd#*n3#Q0>n{D}iE*hbYbcvOR+{+r zqE`jhZ}~MvR_5SsSh4y?#3Wy>^T+55ZY(XV7(N$5dfvQ^kgjpTNtoccc;p$M3q;ej zE$~n}=bqphR=h(cwiHvHGD$m#f$Wal7l6&;n4xC4C}a0L#7d)} zSJ_(eVH=ClVf#^VoVjUJu;?GY*-p;=>Q&_356L^NQ|1h|)BEy$OkcBRxZ?#Vqke>b zD8PXWE1m@ysma72@W`*Pd@Fz`9i0=r@9QNB+G0k`WS;oofVpHgSv`$!+_5lzM{ShL zYY=YS-Iy`zh{8U@_dB+6@9?Pq z^`riq(LNmMtV||TDP0oQQwDM~`*mxNOU+xiF2B=N^i3lAQP{?qC$vQU3t{Y};G>-} z6_!@qzf=l;n;Ev)h748jtZG6gAS7ltCKd7c{5Tdo#JZ!|b&23}zQKSks z55<@Iico_~f7i=@X|UYI3n5QyWv}JWfjBq1#r|0yBrfi%;IGyTTjw{h&+1cSmaE8+ zTBdLM0tsd6+AR7-8L*hjOLB0-W*(N;i(6`MY7AJ8LouZ=-gNreWNZ}J&H1`>c)btsDQ^Aje zQU$Xapkb%z`l|c24lN;UMuOISvJPej&3Nf`Af4TrLNq%R^XY%buEL6+M87tv4n+^_pe>VYyu+=?~DcfKatozB50h3dcDmL|I>=)U|xF%!=Oh z52={N-nuGY5Nj)`0TDMe5kA{ayPZnHlDu*FbB0ae;K4-r9EnrJS+@Rmk#}_rYucM5~7#r z!GJfD%G2yWNaLqZG|qoL&7IUeaQ!BX%>X3npS04EF|5G8uBk6bnDn~RkaM=mU`4u1 z{kvSaUZ}WOY^+x{iO?98cZ62*n3ZE}YJt~ix7g+HwZ?O}-1Z#yyrx6j*YmaQsNS?V zH_vAnB?LDx2Z>7CG~e6(0tG0E(D8crpLB@H&a3lhO4#b<_`bDJhqbd7R~hQXO6knK z6oXRN;oRS2u{PxB-yC&mruZsI0MuI?_f`y83@KOcy}U)_#`#e%T+!50u8yt4b7 zKdRaUM~oKT9~J8~X`qr;JkNB90+^!WD+PYiOr1>L7gyYiP`7SAc%>j7KQO?x=4}je zzQUTkHASpCT@(8JQJ$SR7j3oQE`7L!veKMme zZBCq2p?HcOA3YMhd}XY&OZ;5$(iLtC`jwKl>xk*UORlWNuzJSWjDIUn`TLL_`Q)X> zW24eJ%crTw#j7;_x4=RTOLvLwRNw_S_RG1tH`e5gMy2_c^P5c1g3D z!|3$B@D5v|>qX8tJAG5*N@2(1wk|KlhIfWG=e#|}`Rb%SiRBn{BF_5_RU_=wBA=@= zB!XNN>^o3H9i8fVH+lnRbr!$)j*;KZ0`T5;f&5dyDy$`!&gQ0D*1bpkghd76IUj7;QKF zG!)lkltngbUw$ohAUn@G^NgUpCThKGlgelgJat zH~nF(=-zWp_hY*J`isMd8FEzni|j_m2Gf_=v1Sw)yA+-kOUFWv_^PR)mcpxr{X%T< zJ%Zi`Vw0NA=dPAJ6L9H;g-a8JD9Hxt0;$UURvSAC02hxRdrssF;J7|H{UDCeHZ#yO ze;F@PuOH#X#h!Y@*ef)^pbz*x88`-+mb+$~1%64M`s@qoGrpE9v zW(MG7>cu+!wp0A5Re||Ca6Zk!^oongFoyuC+c+A;*&ya>S?Z`rCLE%7hnB#JZRrxB zlZ$wX6|YpwTQF}JzB$jZ^MEG?iUXJV;xK$(@#|*)U?pg@iBS#d)G%sCxrS&6wYI|4XHqP^E zm5(fJ!**=y*7NPMeyVvVIUeZ335b?u%SA(kRoRK-h|*Uw2Cc#83qkRm*t7_*U*3_t zh7zm+ALted9CyOGRi>yWVYO@b9PRYjIr8wB;%3zTU7USyL=2)_1DU8K-#l1OvKr+0 z_g7y59W&r8A?Q7>px<=^#QGH!;VS2Wc=)&P&F?98bc{9B2Hy?5=P6?0?#0nE5|?ys zaCw3S31-Cx^zCs}4MYEcAXZY@e4E9apuZ2J-ti&vsmrRr!o3NaK7 zyz#sUGtg6*dfj70p1z!WyZ?7n5|lDYW-#GDUpjyt&xEW93Qn1uD`)?+J#)Ax){3$) zFS@mt-H(75&E{Z?zNfOnywaW=?3pS`j)nysHMN>m7jqemx%tbMWKW*{h`X>+oa)A% z6i^P=qwh{GPioQr&<)9GUN+*?B$aIYNeiR_LNxPKSZXRc^0cR0dZx_EBvW-4tJ5b7 zzpIzdaiti|RjhWB5jHEKMoQ%)yK_l&1<&LU4+TWuxn+2_SM^NQsIql3&9r84x7hTl zonrf>4zo^sJ!T#HJCSI9L(y;GK5D?}|4o1V&N^9&_d9&d*a=QJLSm8R0smc$LT}mN zCPhdxPbt|?3S6{^cQEPAQ>1WVg>3?~rql3LDl&1kFH5nz>fEG&n$AS#5LBW0$=`rO z@($m=$BW3d0j0qfHoAaM0m^?52j^m!pVuM)XW0?P7L zO?PdSYWPjTRzA>!==@68yJurPQhLx6yo^3qGN1F>_z%bbJ+vkI4Iu?3F&cl5Vnu60_vNJOppl*J`!jF2n;8`<|n zl0ykeU{jOer0WWLRvwC&E-lh2i*8sx0fR-C>bm2-HyEjo0Z{EF=6Y4E8KdtRLf!`Y z>7q>9gKJvgoh8p-^e^OeDiBSX8jxg7_Os2cGgI?O?U(AZ?(hXE+sQ9IP)U>$HGsE6 zKBO=)A4u?<+c_*UFw}l4qaXM;S(y@W_Bd~X1FoZi6LuJ`H1F%`)X{#f_vWs`;~0_e z_`8|c7LwG`HHHm5DJf`diw-NjEq6xf_z-)w{|^-bwt5%c>U{L&-L*a?B)MgrQ%-f3ru>6rz7kS5;49XXC0}N-B;U%*TS7kCba9b z7jh<-XP6^chbHgu&5?m(s~p}+GFaJ%zNWwlgrZN}I$#PbzNST+rrb1xQPBut&nA54 z@BX`J&?#tJp+Q$_+uwiv8T*ypNW;H}Bm}9Qdr+^iNx?+bR~!*X-~M?0mI{&Ak3@gU z3Q0?dFmO!AExQwYj>{!ZKvzcG9)`4UXm z)Zs2Ce3+_p)8v)vFgIE>n|#ybw$v#{H?VKgopHQ+t@kHOk7smRkBj9j=7B#^*EPQe}gzPxiYZgJL?4f%Yi#_~KxVsAR!jO9VT zU1uOHz1kI0k2VHm`VQ>Z8{n~4fBh#gzS}?jB)hg|s%y+4DOFdGR3t7;H-ZM#TVS??Fa@d{6j@VFd7_KnA4*cYHlM7L@-{nHgO8~-GU=T}KNRoMz zMoO$r(l+-`%79GR=<|3~F;cgm=;8RI;=nb^N@V}L6Ta`k!Z4qQtX&I?_+Pz`n52?fSk@`IZsUj6>9k{s&cg?Jj~BUjK9}bkY^J!#Id)uPwlyXrEXSdrD!{(X42HHO}4$XVM7*1sg;|{rzv*!<=ZKX zn}-GYDS4+&v~8b#=DXf{-W@N{n&&`Y!{}T@9L;DD5QiZwkvEev-tx90^&ORg64hjb z-11`f7_ib@7hPX*Vu6>{@k2yU2>uA*6MVf^hgL23-bt(3 zcbwe>fyxIDu6=jz=^$hD>kRSmQ{w3RJY;qrNIsB3>Esc(An$Q~uJL^Q3O(D&!Xn9} z&C$OUm28q|EGe;6o~8PAksx9jX$2Sxb?qwm`O#lTHx zdh_Xo?~>nOz{Sg4&cH+Pk_UE2L^`yrCAU z*n^uw?@0@MOMf2teeE?9ikV3_*w?_e)`;w12^PrvhoKV2z7D1qY4HTHqA0c4;lu!O z=@j?fGaiL2+;+K?8pk`=3zvyO5?Mg!S7E?Rj511O4jU&kabdLx&uw(|Sl{dh8C2m6 z$X-IiZwz>L%{;k8TkkUaS9DYPG33Z0H$4(96t;qj9I)%}PvrxTc>uidp@G5mKHxS(&+{LLNqs)Lpm_)J8jP7VO;C*GM1Rg0aVxdF3!qqwRk}d6E>4UTwSBTyY8Y3mqDI z3A{hnc&OXT=y>z!Taw+iZAH}gsppmN*4ta$p_7E>z{lacY218j?eGFZvtp<643r$S zV(}YMW)$_?v9?YKNe`msi%$yoH z%A4y9@NgUl4|roB%J;Y#%nZlgEbQw=>HXe%9xm$|^h?|%j6&V!in!}oVdtIb8J^Z3 zTs6|&rH$JR^hjI=_Wc94Aw&-@mt2izVFNA+}2qZb$upm5RNNOCko7d=PHOt6Zg>U)9Fj{1@r>jK3Kv>AKT z2a+LNbo{A-vU_a@HgaSSgG!1CmmK&u0m<%`$m7aVC6o279LqK*+R|YlsI3ikMeNj> zJIT7}XQ3rSHr|GW6(6Rw#pHrayX-Ml_CdH;W^R%4Zt6TE1!9?w$fYc)s+d+4 z^j5+!N{@tlCH{k+DOv&Y?1h5h^ZoVn${;?=WCZ}T%*vq_CnMyiEfAsqvOH-(g;MzA zEyXvaG5GTFnj>#z?Dx2j)C?Wo%KHF2dsFJnO&%1!IXYOF;z7n+C-FE&jE_}xW}yd* z3(yybJ1DMQe<0H1TY@K^h{>0j2C9@-oxXV5M0vpvw`hcpr1z?BO?O;*d$C#gycO*k z*T0|xu5-%rsAx0KvB*YCzb*0*1V_Ye6wWqxuF=GmxfVawPHK#{_h;tFWJ~X`2S89W zvp1Ps%jtLpf|TRQICEE;1%G7)ohAZM0WC8VgdblxDwh?eVUxVw}76t9GqFL(>70QMHJ@ynsz4w;sAbCx} zp{y)z*%oaQjRMTylheaz;$uY~opI_vuW}wd((A{=jK@_OG23-7>^;{?Z(J^^UX`sk zoqldvTk!nl(MU@WCo2|0u(pP%bhR@>TUum}1I~7Iy^RCwlII(^DA{((V^Z;!2UzmNl z0{d+N8p6>;L}nA9y*ueT#yn{^Hoxv;IsN9y7eJ zG1Up=T(l;&uu`wUR1xL(L?fo6`*Yg^#L2>zn@@}A;doVTxHFCW?0-2UVB~Gv*^hd`R0WE!iN?g(#R=Ff-|X@sm2`78FBu!!UL_Ix-jjHM z)z6#d=bY&s-ow5e7ej=xOSqGb{Mm~AOEQGfnL{n{=ud*tW0MjICDu5Xy>L2+Nn}UI zbkwxlHnB*&1`gwQm1=f`O8uWV(6K6+6<(aGJh)K>m;@B{ z=vT%fd&+QbrAnr~MoPfvpB6Dg^lDp!j(CAP+T2$-(gC(}q7ZRXk>ju)+`@~o?R;A4 z*1N-ibNfa7ryd0{)4}8LKfg>Kuh`0I z0R$mdkf4mB84%g9r%9)Z;M6wR3<(RSOK6W^sT9rV7xo~Knl6ZH=UIVzb>M>-m5V0- z{Vf3tW=Tj-bTIbh=r3~__g_h}YQLumspNg?yn`9j^wIpjOSQ6Hmu!@TQ ge>X}0Z^OaKqoPWj{M^dwkN*%=B`w7&`H!Lh15g(U+W-In literal 0 HcmV?d00001 diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square30x30Logo.png b/apps/halidoscope/frontend/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..621970023096ed9f494ba18ace15421a45cd65fa GIT binary patch literal 903 zcmV;219<$2P)2 z+CUKPMqaqGiH;zb!R4$B-WXS^YzQr=@UH>k4?*L)&R=zYjBrZenKdc9|JlS$SO*RJ zKt8FSTDAdk1g_WPAO!p^V!AuL;Lm;uQyV;zKq)J3i(;q*;k+pD%f3eltU`PYdy9(k0&%` zuWAPcV6|-y?|?7O1W!KSK}pbk8#~!|FA@(VJkt^V@0lio{afoAeo*f&$W2s6${5!1eKvAGD2$GZwSB98L2ZVS- zKn8ENRkZ*sb!@QugOrQNK3(sy1v%J#m|rpB+h|Nkqa3FRT>74xSs{#&saU2Lf!_Iq zKmuKAESh`gs!fneGWn+nf}l?7jE$HW!Af&vE5=G!QU)U2v&HLIBGXKk4nQx{hsHjL zLPMAo5=*uInFbq7(aa`Y2VX5wCmaeqvECOFv)a>0t>ZaEb*cJccER=BB?KFZhV$c^ znL*l8x*UYZv4WK|j?~Jt6~~F%{pk~z5A*>^M`?r5m9@RJ_x|uEtX(6Vk@Y()MVto* z93wr)%3m%|#OZ~srm>zF(JvDuTq*@;d&^>_BJm5hOU`3FjG70L#Vzv9I?`<7$T@

jU?lMi@tgxr7CqX_r3uw^y4tVU3Pm0sw;|1WSUO%?=bG`*Kmz6u4{#ti;T7AWIBAEh!(Y zz>O01&#X?Ds@L)Sb{CkG#Yz4$3o d@96)?#cz^xWoA}>B$xmI002ovPDHLkV1l3&k#zt7 literal 0 HcmV?d00001 diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square310x310Logo.png b/apps/halidoscope/frontend/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f9bc04839491e66c07b16ab03743c0c53b4109cc GIT binary patch literal 8591 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 JPDHLkV1lm|RXG3v literal 0 HcmV?d00001 diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square71x71Logo.png b/apps/halidoscope/frontend/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..63440d7984936a9caa89275928d8dce97e4d033b GIT binary patch literal 2011 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∓t$dRJGIadE?vc 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 Kx8Vjl{{H|h@<;Lj literal 0 HcmV?d00001 diff --git a/apps/halidoscope/frontend/src-tauri/icons/icon.ico b/apps/halidoscope/frontend/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3636e4b22ba65db9061cd60a77b02c92022dfd6 GIT binary patch literal 86642 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 AKmY&$ literal 0 HcmV?d00001 diff --git a/apps/halidoscope/frontend/src-tauri/icons/icon.png b/apps/halidoscope/frontend/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1cd2619e0b5ec089cbba5ec7b03ddf2b1dfceb6 GIT binary patch literal 14183 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

5l#v3A#xI3*z~1~fvo9Q(N^(==!|_FZ z*duZ=+M1~)8E|otX8KNZlr?qels#x_1Xq@9IIw~@9uAREJVH)Xw^}UclF6327}E42 zT)E&?U%TK?(+K7%R!`H5oX0i)4Qn5??Iw3p5J~6_u+aWehY{DSn}3V2p$bgjnAu?o)v@iC254fXeMv50$9YrpU`N?u@QIWs)T?SP|fa}(|9 zqAX+!7`cx=4)cCBg5h~pu(?@9`)aCr#oyz$ld=#RFxYCNZCZls@4v2~*e-t6PEVvV z&bbK3b3wt(Coc!ufAbXXC<**#HQ%J9k`New6iG<5RjtO4XVO?dCvwxD{kJ#tfQr(X zg^NTwF-FwAeS_{V4bfel8l`~NbfrTR2s!G>WduFWxH(t~aK4q=6rEE^$+Uox>gJO2 z{L<;6Q6nHa5#ZEM>H58not!)z(6*_=^~8}jWf*IG$AUKVWOZ4?)GfF z+BM#*wKKmLFD7E~W3U!$IVm$k_k1f&Kz6WV8@55P?r~bcg-Za-!rvW?ns&)KOGT2~ zlkAyqhQj=P$Eg3w#K~}zH@J5bo-BfHjInKSz$@?+Z)NPD4pHj^_Qxmi`UqoTy=`sV zLVxrXGuBr=QRm|}wg75yetQQK4fY3#P_~J}zEfPnb2C4Wo!E(d*(cA;b?7$g2in<( zPn)ghX}nzJPmb6(3Dpeg_GW~Hc}Lt=lgsSZz z!5QXyz7KaR;D`3Ee}d`af{H>WWZ|Io1QI3~4Ll_`g1(cRnhLK73Ro)7zPCd={1W2x zRp%Xlvv4>!<2@}$hz|!V{T}_eHx2xkLl^hQoZTCnsjCl|W_@5Fx2(+j0ogy&Y+;L- z<)G$*CiN7hOm^s!{U>1F7U=iNk{+u~dAC!eDz%=|glFW0jEZU1&o(G_c#wTxUjnG} z#cg3>jEpUi#Mlq@t?Msg_#geK^Lx@DyHWf7=AS5vVyM7YOjvUVCfcpVR<(+5!H?9- zySI6s>o3m&*zr||=wcPGyBkQV`EWJl@bH8qobjOp+sXL*)=&yX)8aAbf~tGv?a2SN zu^Ddo-z?DWk9h9Yz#5p^NU#x~wYSd?H@w@!2Gb4G)6-utEMV~~M85Br5ff(v5O1|T z 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 Result { + std::env::current_dir() + .map_err(|e| e.to_string()) + .and_then(|p| { + p.parent() + .map(|parent| parent.to_string_lossy().into_owned()) + .ok_or_else(|| "no parent directory".to_string()) + }) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .setup(|app| { + #[cfg(desktop)] + app.handle().plugin(tauri_plugin_cli::init())?; + Ok(()) + }) + .plugin(tauri_plugin_opener::init()) + .invoke_handler(tauri::generate_handler![get_cwd]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/apps/halidoscope/frontend/src-tauri/src/main.rs b/apps/halidoscope/frontend/src-tauri/src/main.rs new file mode 100644 index 000000000000..e26959fff12b --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + frontend_lib::run() +} diff --git a/apps/halidoscope/frontend/src-tauri/tauri.conf.json b/apps/halidoscope/frontend/src-tauri/tauri.conf.json new file mode 100644 index 000000000000..95215cf66b94 --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/tauri.conf.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "halidoscope", + "version": "0.1.0", + "identifier": "com.halide.halidescope", + "build": { + "beforeDevCommand": "pnpm dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "pnpm build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "halidoscope", + "width": 800, + "height": 600 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + }, + "plugins": { + "cli": { + "args": [ + { + "name": "trace", + "short": "t", + "takesValue": true, + "description": "Path to .hltrace file to load on startup" + } + ] + } + } +} diff --git a/apps/halidoscope/frontend/src/App.css b/apps/halidoscope/frontend/src/App.css new file mode 100644 index 000000000000..65ad14c03397 --- /dev/null +++ b/apps/halidoscope/frontend/src/App.css @@ -0,0 +1,17 @@ +@import "tailwindcss"; + +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} diff --git a/apps/halidoscope/frontend/src/App.tsx b/apps/halidoscope/frontend/src/App.tsx new file mode 100644 index 000000000000..2192ff576c10 --- /dev/null +++ b/apps/halidoscope/frontend/src/App.tsx @@ -0,0 +1,128 @@ +import { invoke } from "@tauri-apps/api/core"; +import { getMatches } from "@tauri-apps/plugin-cli"; +import { useEffect, useRef, useState } from "react"; + +import FuncCanvas from "./components/FuncCanvas"; +import { Sidebar } from "./components/Sidebar"; +import { FuncStats } from "./types"; +import { BACKEND_ENDPOINT, WS_ENDPOINT } from "./utils/constants"; + +import "./App.css"; + +async function loadTracePath(path: string, signal: AbortSignal) { + const response = await fetch(`${BACKEND_ENDPOINT}/load-path`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path }), + signal, + }); + + if (!response.ok) { + throw new Error(`Failed to load trace: ${response.statusText}`); + } + + return response.json(); +} + +async function deregisterTrace(session: string) { + const response = await fetch(`${BACKEND_ENDPOINT}/session/${session}`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }); + + if (!response.ok) { + throw new Error(`Failed to deregister trace: ${response.statusText}`); + } + + return response.json(); +} + +function App() { + const [session, setSession] = useState(""); + const [funcs, setFuncs] = useState>({}); + const wsRef = useRef(null); + + useEffect(() => { + const controller = new AbortController(); + + async function loadTraceFromCLI() { + const matches = await getMatches(); + const tracePath = matches.args.trace?.value; + + if (typeof tracePath === "string") { + const resolved = tracePath.startsWith("/") + ? tracePath + : `${await invoke("get_cwd")}/${tracePath}`; + + try { + const { session_id, funcs } = await loadTracePath( + resolved, + controller.signal, + ); + + setSession(session_id); + setFuncs(funcs); + + const ws = new WebSocket(`${WS_ENDPOINT}/ws/${session_id}`); + wsRef.current = ws; + ws.onopen = () => + console.log("WebSocket connected: session=%s", session_id); + ws.onmessage = (event) => + console.log("WebSocket message:", JSON.parse(event.data)); + ws.onerror = (err) => console.error("WebSocket error:", err); + ws.onclose = () => { + wsRef.current = null; + }; + } catch (err) { + if ((err as Error).name !== "AbortError") { + console.error("Error loading trace from CLI: ", err); + } + } + } + } + + loadTraceFromCLI(); + + return () => { + controller.abort(); + wsRef.current?.close(); + if (session) { + deregisterTrace(session) + .then((res) => console.log("Successfully removed trace.", res.json())) + .catch((err) => console.error(err)); + } + }; + }, []); + + return ( +

+ +
+ {Object.keys(funcs).length > 0 ? ( + Object.entries(funcs).map(([name, stats]) => ( + + )) + ) : ( +

Loading trace...

+ )} +
+
+ ); +} + +export default App; diff --git a/apps/halidoscope/frontend/src/components/FuncCanvas.tsx b/apps/halidoscope/frontend/src/components/FuncCanvas.tsx new file mode 100644 index 000000000000..6db5b6165118 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/FuncCanvas.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; + +interface FuncCanvasProps { + name: string; + width: number; + height: number; + xs: number[]; + ys: number[]; + values: number[]; +} + +const MAX_DISPLAY_PX = 800; + +function FuncCanvas({ name, width, height, xs, ys, values }: FuncCanvasProps) { + const canvas = React.useRef(null); + const scale = Math.min(1, MAX_DISPLAY_PX / width, MAX_DISPLAY_PX / height); + + React.useEffect(() => { + const ctx = canvas.current?.getContext("2d"); + + if (ctx) { + const imageData = ctx.createImageData(width, height); + for (let i = 0; i < imageData.data.length; i += 4) { + imageData.data[i + 3] = 255; + } + + ctx.putImageData(imageData, 0, 0); + } + + // if (ctx) { + // const imageData = ctx.createImageData(width, height); + + // for (let i = 0; i < xs.length; i++) { + // const idx = 4 * (ys[i] * width + xs[i]); + // imageData.data[idx + 0] = values[i]; // R + // imageData.data[idx + 1] = values[i]; // G + // imageData.data[idx + 2] = values[i]; // B + // imageData.data[idx + 3] = 255; + // } + + // ctx.putImageData(imageData, 0, 0); + // } + }, [xs, ys, values, width, height]); + + return ( +
+ + {name} + {scale < 1 && ( + + {Math.round(scale * 100)}% + + )} + + +
+ ); +} + +export default FuncCanvas; diff --git a/apps/halidoscope/frontend/src/components/Sidebar.tsx b/apps/halidoscope/frontend/src/components/Sidebar.tsx new file mode 100644 index 000000000000..1520851cf6e2 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/Sidebar.tsx @@ -0,0 +1,22 @@ +import { FuncStats } from "../types"; + +interface SidebarProps { + funcs: Record; +} + +export function Sidebar({ funcs }: SidebarProps) { + return ( + + ); +} diff --git a/apps/halidoscope/frontend/src/main.tsx b/apps/halidoscope/frontend/src/main.tsx new file mode 100644 index 000000000000..2be325ed2578 --- /dev/null +++ b/apps/halidoscope/frontend/src/main.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/apps/halidoscope/frontend/src/types/index.ts b/apps/halidoscope/frontend/src/types/index.ts new file mode 100644 index 000000000000..bdc69664243b --- /dev/null +++ b/apps/halidoscope/frontend/src/types/index.ts @@ -0,0 +1,7 @@ +export interface FuncStats { + name: string; + min_coords: number[]; + max_coords: number[]; + min_value: number; + max_value: number; +} diff --git a/apps/halidoscope/frontend/src/utils/constants.ts b/apps/halidoscope/frontend/src/utils/constants.ts new file mode 100644 index 000000000000..fd694eeebb07 --- /dev/null +++ b/apps/halidoscope/frontend/src/utils/constants.ts @@ -0,0 +1,2 @@ +export const BACKEND_ENDPOINT = "http://localhost:8765"; +export const WS_ENDPOINT = "ws://localhost:8765"; diff --git a/apps/halidoscope/frontend/src/vite-env.d.ts b/apps/halidoscope/frontend/src/vite-env.d.ts new file mode 100644 index 000000000000..11f02fe2a006 --- /dev/null +++ b/apps/halidoscope/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/halidoscope/frontend/tsconfig.json b/apps/halidoscope/frontend/tsconfig.json new file mode 100644 index 000000000000..9479566170ff --- /dev/null +++ b/apps/halidoscope/frontend/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/apps/halidoscope/frontend/tsconfig.node.json b/apps/halidoscope/frontend/tsconfig.node.json new file mode 100644 index 000000000000..b5a343184303 --- /dev/null +++ b/apps/halidoscope/frontend/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/apps/halidoscope/frontend/vite.config.ts b/apps/halidoscope/frontend/vite.config.ts new file mode 100644 index 000000000000..429a2de5795c --- /dev/null +++ b/apps/halidoscope/frontend/vite.config.ts @@ -0,0 +1,33 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +// @ts-expect-error process is a nodejs global +const host = process.env.TAURI_DEV_HOST; + +// https://vite.dev/config/ +export default defineConfig(async () => ({ + plugins: [react(), tailwindcss()], + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent Vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, +})); From 047b256dbee3114bbe6071cdef62eda505da85bb Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 4 Jun 2026 16:12:09 -0700 Subject: [PATCH 08/67] feat: Support full playback and forward scrubbing of Halide traces. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/backend/backend/main.py | 1 + apps/halidoscope/frontend/eslint.config.mjs | 18 + apps/halidoscope/frontend/package.json | 9 + apps/halidoscope/frontend/pnpm-lock.yaml | 4402 ++++++++++++++--- .../frontend/src-tauri/tauri.conf.json | 8 +- apps/halidoscope/frontend/src/App.css | 10 + apps/halidoscope/frontend/src/App.tsx | 140 +- .../frontend/src/components/Canvas.tsx | 42 + .../frontend/src/components/FuncCanvas.tsx | 93 +- .../frontend/src/components/Sidebar.tsx | 32 +- .../frontend/src/components/Timeline.tsx | 254 + .../frontend/src/hooks/canvas-registry.ts | 93 + apps/halidoscope/frontend/src/types/index.ts | 32 + apps/halidoscope/frontend/src/utils/api.ts | 61 + .../frontend/src/utils/constants.ts | 7 + apps/halidoscope/frontend/src/utils/func.ts | 18 + apps/halidoscope/frontend/src/utils/graph.ts | 95 + 17 files changed, 4503 insertions(+), 812 deletions(-) create mode 100644 apps/halidoscope/frontend/eslint.config.mjs create mode 100644 apps/halidoscope/frontend/src/components/Canvas.tsx create mode 100644 apps/halidoscope/frontend/src/components/Timeline.tsx create mode 100644 apps/halidoscope/frontend/src/hooks/canvas-registry.ts create mode 100644 apps/halidoscope/frontend/src/utils/api.ts create mode 100644 apps/halidoscope/frontend/src/utils/func.ts create mode 100644 apps/halidoscope/frontend/src/utils/graph.ts diff --git a/apps/halidoscope/backend/backend/main.py b/apps/halidoscope/backend/backend/main.py index daf5dedab3e1..afe00b0fd296 100644 --- a/apps/halidoscope/backend/backend/main.py +++ b/apps/halidoscope/backend/backend/main.py @@ -49,6 +49,7 @@ def _register_trace(trace: Any) -> dict[str, Any]: session_id = str(uuid.uuid4()) payload = { "session_id": session_id, + "num_packets": len(trace), "funcs": {name: _serialize_func_stats(s) for name, s in trace.funcs.items()}, "dag_edges": {k: list(v) for k, v in trace.dag_edges.items()}, "pipelines": {str(k): v for k, v in trace.pipelines.items()}, diff --git a/apps/halidoscope/frontend/eslint.config.mjs b/apps/halidoscope/frontend/eslint.config.mjs new file mode 100644 index 000000000000..65640e89e639 --- /dev/null +++ b/apps/halidoscope/frontend/eslint.config.mjs @@ -0,0 +1,18 @@ +// @ts-check + +import js from "@eslint/js"; +import { defineConfig, globalIgnores } from "eslint/config"; +import reactHooks from "eslint-plugin-react-hooks"; +import tseslint from "typescript-eslint"; + +export default defineConfig([ + globalIgnores(["src-tauri/**"]), + { + files: ["**/*.{js,ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + ], + }, +]); diff --git a/apps/halidoscope/frontend/package.json b/apps/halidoscope/frontend/package.json index 1c500766ab4c..8a9abdb9fb2f 100644 --- a/apps/halidoscope/frontend/package.json +++ b/apps/halidoscope/frontend/package.json @@ -10,20 +10,29 @@ "tauri": "tauri" }, "dependencies": { + "@dagrejs/dagre": "^3.0.0", "@tailwindcss/vite": "^4.3.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-cli": "^2.4.1", "@tauri-apps/plugin-opener": "^2", + "@xyflow/react": "^12.11.0", + "d3": "^7.9.0", + "radix-ui": "^1.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", "tailwindcss": "^4.3.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@tauri-apps/cli": "^2", + "@types/d3": "^7.4.3", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "eslint": "^10.4.1", + "eslint-plugin-react-hooks": "^7.1.1", "typescript": "~5.8.3", + "typescript-eslint": "^8.60.1", "vite": "^7.0.4" } } diff --git a/apps/halidoscope/frontend/pnpm-lock.yaml b/apps/halidoscope/frontend/pnpm-lock.yaml index a40d43231dee..df525334b55d 100644 --- a/apps/halidoscope/frontend/pnpm-lock.yaml +++ b/apps/halidoscope/frontend/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@dagrejs/dagre': + specifier: ^3.0.0 + version: 3.0.0 '@tailwindcss/vite': specifier: ^4.3.0 version: 4.3.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0)) @@ -20,6 +23,15 @@ importers: '@tauri-apps/plugin-opener': specifier: ^2 version: 2.5.4 + '@xyflow/react': + specifier: ^12.11.0 + version: 12.11.0(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + d3: + specifier: ^7.9.0 + version: 7.9.0 + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: specifier: ^19.1.0 version: 19.2.6 @@ -30,9 +42,15 @@ importers: specifier: ^4.3.0 version: 4.3.0 devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.4.1(jiti@2.7.0)) '@tauri-apps/cli': specifier: ^2 version: 2.11.2 + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 '@types/react': specifier: ^19.1.8 version: 19.2.15 @@ -42,9 +60,18 @@ importers: '@vitejs/plugin-react': specifier: ^4.6.0 version: 4.7.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0)) + eslint: + specifier: ^10.4.1 + version: 10.4.1(jiti@2.7.0) + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@10.4.1(jiti@2.7.0)) typescript: specifier: ~5.8.3 version: 5.8.3 + typescript-eslint: + specifier: ^8.60.1 + version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) vite: specifier: ^7.0.4 version: 7.3.5(jiti@2.7.0)(lightningcss@1.32.0) @@ -134,6 +161,12 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@dagrejs/dagre@3.0.0': + resolution: {integrity: sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==} + + '@dagrejs/graphlib@4.0.1': + resolution: {integrity: sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==} + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} @@ -290,6 +323,80 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -306,848 +413,3033 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@rolldown/pluginutils@1.0.0-beta.27': - resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} - '@rollup/rollup-android-arm-eabi@4.61.0': - resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} - cpu: [arm] - os: [android] + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - '@rollup/rollup-android-arm64@4.61.0': - resolution: {integrity: sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==} - cpu: [arm64] - os: [android] + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-darwin-arm64@4.61.0': - resolution: {integrity: sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==} - cpu: [arm64] - os: [darwin] + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-darwin-x64@4.61.0': - resolution: {integrity: sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==} - cpu: [x64] - os: [darwin] + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-freebsd-arm64@4.61.0': - resolution: {integrity: sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==} - cpu: [arm64] - os: [freebsd] + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-freebsd-x64@4.61.0': - resolution: {integrity: sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==} - cpu: [x64] - os: [freebsd] + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.61.0': - resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} - cpu: [arm] - os: [linux] - libc: [glibc] + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-arm-musleabihf@4.61.0': - resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} - cpu: [arm] - os: [linux] - libc: [musl] + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-arm64-gnu@4.61.0': - resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} - cpu: [arm64] - os: [linux] - libc: [glibc] + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-arm64-musl@4.61.0': - resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} - cpu: [arm64] - os: [linux] - libc: [musl] + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-loong64-gnu@4.61.0': - resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} - cpu: [loong64] - os: [linux] - libc: [glibc] + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@rollup/rollup-linux-loong64-musl@4.61.0': - resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} - cpu: [loong64] - os: [linux] - libc: [musl] + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-ppc64-gnu@4.61.0': - resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@rollup/rollup-linux-ppc64-musl@4.61.0': - resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} - cpu: [ppc64] - os: [linux] - libc: [musl] + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-riscv64-gnu@4.61.0': - resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@rollup/rollup-linux-riscv64-musl@4.61.0': - resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} - cpu: [riscv64] - os: [linux] - libc: [musl] + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-s390x-gnu@4.61.0': - resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} - cpu: [s390x] - os: [linux] - libc: [glibc] + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-x64-gnu@4.61.0': - resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} - cpu: [x64] - os: [linux] - libc: [glibc] + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@rollup/rollup-linux-x64-musl@4.61.0': - resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} - cpu: [x64] - os: [linux] - libc: [musl] + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-openbsd-x64@4.61.0': - resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} - cpu: [x64] - os: [openbsd] + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-openharmony-arm64@4.61.0': - resolution: {integrity: sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==} - cpu: [arm64] - os: [openharmony] + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-win32-arm64-msvc@4.61.0': - resolution: {integrity: sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==} - cpu: [arm64] - os: [win32] + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@rollup/rollup-win32-ia32-msvc@4.61.0': - resolution: {integrity: sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==} - cpu: [ia32] - os: [win32] + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-win32-x64-gnu@4.61.0': - resolution: {integrity: sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==} - cpu: [x64] + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.61.0': + resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.0': + resolution: {integrity: sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.0': + resolution: {integrity: sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.0': + resolution: {integrity: sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.0': + resolution: {integrity: sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.0': + resolution: {integrity: sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.61.0': + resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.61.0': + resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.61.0': + resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.61.0': + resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.61.0': + resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.61.0': + resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.61.0': + resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.61.0': + resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.61.0': + resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.61.0': + resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.0': + resolution: {integrity: sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.0': + resolution: {integrity: sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.0': + resolution: {integrity: sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.0': + resolution: {integrity: sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.0': + resolution: {integrity: sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tauri-apps/api@2.11.0': + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} + + '@tauri-apps/cli-darwin-arm64@2.11.2': + resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.2': + resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.2': + resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} + engines: {node: '>= 10'} + cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.61.0': - resolution: {integrity: sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==} - cpu: [x64] - os: [win32] + '@tauri-apps/cli@2.11.2': + resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-cli@2.4.1': + resolution: {integrity: sha512-8JXofQFI5cmiGolh1PlU4hzE2YJgrgB1lyaztyBYiiMCy13luVxBXaXChYPeqMkUo46J1UadxvYdjRjj0E8zaw==} + + '@tauri-apps/plugin-opener@2.5.4': + resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + + '@typescript-eslint/eslint-plugin@8.60.1': + resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.60.1': + resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.60.1': + resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.60.1': + resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.60.1': + resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.60.1': + resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.60.1': + resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.60.1': + resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.60.1': + resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.60.1': + resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@xyflow/react@12.11.0': + resolution: {integrity: sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==} + peerDependencies: + '@types/react': '>=17' + '@types/react-dom': '>=17' + react: '>=17' + react-dom: '>=17' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@xyflow/system@0.0.77': + resolution: {integrity: sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.4.1: + resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} - '@tailwindcss/node@4.3.0': - resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} - '@tailwindcss/oxide-android-arm64@4.3.0': - resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} - engines: {node: '>= 20'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.0': - resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} - engines: {node: '>= 20'} + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.0': - resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} - engines: {node: '>= 20'} + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.0': - resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} - engines: {node: '>= 20'} + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': - resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} - engines: {node: '>= 20'} + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': - resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} - engines: {node: '>= 20'} + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': - resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} - engines: {node: '>= 20'} + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': - resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rollup@4.61.0: + resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.60.1: + resolution: {integrity: sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true - '@tailwindcss/oxide-linux-x64-musl@4.3.0': - resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} - '@tailwindcss/oxide-wasm32-wasi@4.3.0': - resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': - resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': - resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 - '@tailwindcss/oxide@4.3.0': - resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} - engines: {node: '>= 20'} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - '@tailwindcss/vite@4.3.0': - resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} peerDependencies: - vite: ^5.2.0 || ^6 || ^7 || ^8 + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true - '@tauri-apps/api@2.11.0': - resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} +snapshots: - '@tauri-apps/cli-darwin-arm64@2.11.2': - resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 - '@tauri-apps/cli-darwin-x64@2.11.2': - resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] + '@babel/compat-data@7.29.7': {} - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': - resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - '@tauri-apps/cli-linux-arm64-gnu@2.11.2': - resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 - '@tauri-apps/cli-linux-arm64-musl@2.11.2': - resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 - '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': - resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - libc: [glibc] + '@babel/helper-globals@7.29.7': {} - '@tauri-apps/cli-linux-x64-gnu@2.11.2': - resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color - '@tauri-apps/cli-linux-x64-musl@2.11.2': - resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color - '@tauri-apps/cli-win32-arm64-msvc@2.11.2': - resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] + '@babel/helper-plugin-utils@7.29.7': {} - '@tauri-apps/cli-win32-ia32-msvc@2.11.2': - resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] + '@babel/helper-string-parser@7.29.7': {} - '@tauri-apps/cli-win32-x64-msvc@2.11.2': - resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] + '@babel/helper-validator-identifier@7.29.7': {} - '@tauri-apps/cli@2.11.2': - resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} - engines: {node: '>= 10'} - hasBin: true + '@babel/helper-validator-option@7.29.7': {} - '@tauri-apps/plugin-cli@2.4.1': - resolution: {integrity: sha512-8JXofQFI5cmiGolh1PlU4hzE2YJgrgB1lyaztyBYiiMCy13luVxBXaXChYPeqMkUo46J1UadxvYdjRjj0E8zaw==} + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 - '@tauri-apps/plugin-opener@2.5.4': - resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@dagrejs/dagre@3.0.0': + dependencies: + '@dagrejs/graphlib': 4.0.1 - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 + '@dagrejs/graphlib@4.0.1': {} - '@types/react@19.2.15': - resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + '@esbuild/aix-ppc64@0.27.7': + optional: true - '@vitejs/plugin-react@4.7.0': - resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@esbuild/android-arm64@0.27.7': + optional: true - baseline-browser-mapping@2.10.33: - resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} - engines: {node: '>=6.0.0'} - hasBin: true + '@esbuild/android-arm@0.27.7': + optional: true - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + '@esbuild/android-x64@0.27.7': + optional: true - caniuse-lite@1.0.30001793: - resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + '@esbuild/darwin-arm64@0.27.7': + optional: true - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + '@esbuild/darwin-x64@0.27.7': + optional: true - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + '@esbuild/freebsd-arm64@0.27.7': + optional: true - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} + '@esbuild/linux-arm64@0.27.7': + optional: true - electron-to-chromium@1.5.364: - resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + '@esbuild/linux-arm@0.27.7': + optional: true - enhanced-resolve@5.22.1: - resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} - engines: {node: '>=10.13.0'} + '@esbuild/linux-ia32@0.27.7': + optional: true - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true + '@esbuild/linux-loong64@0.27.7': + optional: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + '@esbuild/linux-mips64el@0.27.7': + optional: true - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] + '@esbuild/linux-riscv64@0.27.7': + optional: true - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} + '@esbuild/linux-s390x@0.27.7': + optional: true - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + '@esbuild/linux-x64@0.27.7': + optional: true - jiti@2.7.0: - resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} - hasBin: true + '@esbuild/netbsd-arm64@0.27.7': + optional: true - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + '@esbuild/netbsd-x64@0.27.7': + optional: true - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true + '@esbuild/openbsd-arm64@0.27.7': + optional: true - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true + '@esbuild/openbsd-x64@0.27.7': + optional: true - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] + '@esbuild/openharmony-arm64@0.27.7': + optional: true - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] + '@esbuild/sunos-x64@0.27.7': + optional: true - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] + '@esbuild/win32-arm64@0.27.7': + optional: true - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] + '@esbuild/win32-ia32@0.27.7': + optional: true - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] + '@esbuild/win32-x64@0.27.7': + optional: true - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.7.0))': + dependencies: + eslint: 10.4.1(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] + '@eslint-community/regexpp@4.12.2': {} - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] + '@eslint/js@10.0.1(eslint@10.4.1(jiti@2.7.0))': + optionalDependencies: + eslint: 10.4.1(jiti@2.7.0) - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} + '@eslint/object-schema@3.0.5': {} - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true + '@floating-ui/react-dom@2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - node-releases@2.0.46: - resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} - engines: {node: '>=18'} + '@floating-ui/utils@0.2.11': {} - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} - engines: {node: ^10 || ^12 || >=14} + '@humanfs/types@0.15.0': {} - react-dom@19.2.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} - peerDependencies: - react: ^19.2.6 + '@humanwhocodes/module-importer@1.0.1': {} - react-refresh@0.17.0: - resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} - engines: {node: '>=0.10.0'} + '@humanwhocodes/retry@0.4.3': {} - react@19.2.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} - engines: {node: '>=0.10.0'} + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - rollup@4.61.0: - resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + '@jridgewell/resolve-uri@3.1.2': {} - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true + '@jridgewell/sourcemap-codec@1.5.5': {} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 - tailwindcss@4.3.0: - resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + '@radix-ui/number@1.1.1': {} - tapable@2.3.3: - resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} - engines: {node: '>=6'} + '@radix-ui/primitive@1.1.3': {} - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} - engines: {node: '>=14.17'} - hasBin: true + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - vite@7.3.5: - resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) -snapshots: + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/code-frame@7.29.7': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/compat-data@7.29.7': {} + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/core@7.29.7': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/generator@7.29.7': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@babel/helper-compilation-targets@7.29.7': + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/compat-data': 7.29.7 - '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 - lru-cache: 5.1.1 - semver: 6.3.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/helper-globals@7.29.7': {} + '@radix-ui/react-context@1.1.2(@types/react@19.2.15)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@babel/helper-module-imports@7.29.7': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - transitivePeerDependencies: - - supports-color + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + aria-hidden: 1.2.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.15)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 - transitivePeerDependencies: - - supports-color + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@babel/helper-plugin-utils@7.29.7': {} + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/helper-string-parser@7.29.7': {} + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/helper-validator-identifier@7.29.7': {} + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.15)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@babel/helper-validator-option@7.29.7': {} + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/helpers@7.29.7': + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/parser@7.29.7': + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/types': 7.29.7 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/template@7.29.7': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + aria-hidden: 1.2.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.15)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/traverse@7.29.7': + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@babel/types@7.29.7': + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/aix-ppc64@0.27.7': - optional: true + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/android-arm64@0.27.7': - optional: true + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + aria-hidden: 1.2.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.15)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/android-arm@0.27.7': - optional: true + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/rect': 1.1.1 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/android-x64@0.27.7': - optional: true + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/darwin-arm64@0.27.7': - optional: true + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/darwin-x64@0.27.7': - optional: true + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/freebsd-arm64@0.27.7': - optional: true + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/freebsd-x64@0.27.7': - optional: true + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/linux-arm64@0.27.7': - optional: true + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/linux-arm@0.27.7': - optional: true + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/linux-ia32@0.27.7': - optional: true + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + aria-hidden: 1.2.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.15)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/linux-loong64@0.27.7': - optional: true + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/linux-mips64el@0.27.7': - optional: true + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/linux-ppc64@0.27.7': - optional: true + '@radix-ui/react-slot@1.2.3(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@esbuild/linux-riscv64@0.27.7': - optional: true + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/linux-s390x@0.27.7': - optional: true + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/linux-x64@0.27.7': - optional: true + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/netbsd-arm64@0.27.7': - optional: true + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/netbsd-x64@0.27.7': - optional: true + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/openbsd-arm64@0.27.7': - optional: true + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/openbsd-x64@0.27.7': - optional: true + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@esbuild/openharmony-arm64@0.27.7': - optional: true + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.15)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@esbuild/sunos-x64@0.27.7': - optional: true + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@esbuild/win32-arm64@0.27.7': - optional: true + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@esbuild/win32-ia32@0.27.7': - optional: true + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@esbuild/win32-x64@0.27.7': - optional: true + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.15)(react@19.2.6)': + dependencies: + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 - '@jridgewell/gen-mapping@0.3.13': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@jridgewell/remapping@2.3.5': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@jridgewell/resolve-uri@3.1.2': {} + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@jridgewell/sourcemap-codec@1.5.5': {} + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.15)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.15 - '@jridgewell/trace-mapping@0.3.31': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + + '@radix-ui/rect@1.1.1': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -1343,44 +3635,258 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 '@tauri-apps/cli-win32-x64-msvc': 2.11.2 - '@tauri-apps/plugin-cli@2.4.1': + '@tauri-apps/plugin-cli@2.4.1': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-opener@2.5.4': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/geojson@7946.0.16': {} + + '@types/json-schema@7.0.15': {} + + '@types/react-dom@19.2.3(@types/react@19.2.15)': + dependencies: + '@types/react': 19.2.15 + + '@types/react@19.2.15': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3)': dependencies: - '@tauri-apps/api': 2.11.0 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.1(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color - '@tauri-apps/plugin-opener@2.5.4': + '@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3)': dependencies: - '@tauri-apps/api': 2.11.0 + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color - '@types/babel__core@7.20.5': + '@typescript-eslint/project-service@8.60.1(typescript@5.8.3)': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.8.3) + '@typescript-eslint/types': 8.60.1 + debug: 4.4.3 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color - '@types/babel__generator@7.27.0': + '@typescript-eslint/scope-manager@8.60.1': dependencies: - '@babel/types': 7.29.7 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 - '@types/babel__template@7.4.4': + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@5.8.3)': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + typescript: 5.8.3 - '@types/babel__traverse@7.28.0': + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3)': dependencies: - '@babel/types': 7.29.7 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) + debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color - '@types/estree@1.0.9': {} + '@typescript-eslint/types@8.60.1': {} - '@types/react-dom@19.2.3(@types/react@19.2.15)': + '@typescript-eslint/typescript-estree@8.60.1(typescript@5.8.3)': dependencies: - '@types/react': 19.2.15 + '@typescript-eslint/project-service': 8.60.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.8.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color - '@types/react@19.2.15': + '@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3)': dependencies: - csstype: 3.2.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.8.3) + eslint: 10.4.1(jiti@2.7.0) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + eslint-visitor-keys: 5.0.1 '@vitejs/plugin-react@4.7.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: @@ -1394,8 +3900,56 @@ snapshots: transitivePeerDependencies: - supports-color + '@xyflow/react@12.11.0(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@xyflow/system': 0.0.77 + classcat: 5.0.5 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + zustand: 4.5.7(@types/react@19.2.15)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + transitivePeerDependencies: + - immer + + '@xyflow/system@0.0.77': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.33: {} + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.33 @@ -1406,16 +3960,186 @@ snapshots: caniuse-lite@1.0.30001793: {} + classcat@5.0.5: {} + + commander@7.2.0: {} + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + debug@4.4.3: dependencies: ms: 2.1.3 + deep-is@0.1.4: {} + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} + electron-to-chromium@1.5.364: {} enhanced-resolve@5.22.1: @@ -1454,25 +4178,173 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@10.4.1(jiti@2.7.0)): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 10.4.1(jiti@2.7.0) + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.4.1(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + fsevents@2.3.3: optional: true gensync@1.0.0-beta.2: {} + get-nonce@1.0.1: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + graceful-fs@4.2.11: {} + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + internmap@2.0.3: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + jiti@2.7.0: {} js-tokens@4.0.0: {} jsesc@3.1.0: {} + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: optional: true @@ -1522,6 +4394,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -1530,12 +4406,39 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + ms@2.1.3: {} nanoid@3.3.12: {} + natural-compare@1.4.0: {} + node-releases@2.0.46: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -1546,6 +4449,73 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + react-dom@19.2.6(react@19.2.6): dependencies: react: 19.2.6 @@ -1553,8 +4523,37 @@ snapshots: react-refresh@0.17.0: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.15)(react@19.2.6): + dependencies: + react: 19.2.6 + react-style-singleton: 2.2.3(@types/react@19.2.15)(react@19.2.6) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.15 + + react-remove-scroll@2.7.2(@types/react@19.2.15)(react@19.2.6): + dependencies: + react: 19.2.6 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.15)(react@19.2.6) + react-style-singleton: 2.2.3(@types/react@19.2.15)(react@19.2.6) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.15)(react@19.2.6) + use-sidecar: 1.1.3(@types/react@19.2.15)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + + react-style-singleton@2.2.3(@types/react@19.2.15)(react@19.2.6): + dependencies: + get-nonce: 1.0.1 + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.15 + react@19.2.6: {} + robust-predicates@3.0.3: {} + rollup@4.61.0: dependencies: '@types/estree': 1.0.9 @@ -1586,10 +4585,22 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.61.0 fsevents: 2.3.3 + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} semver@6.3.1: {} + semver@7.8.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + source-map-js@1.2.1: {} tailwindcss@4.3.0: {} @@ -1601,6 +4612,27 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + ts-api-utils@2.5.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) + eslint: 10.4.1(jiti@2.7.0) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + typescript@5.8.3: {} update-browserslist-db@1.2.3(browserslist@4.28.2): @@ -1609,6 +4641,29 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.15)(react@19.2.6): + dependencies: + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.15 + + use-sidecar@1.1.3(@types/react@19.2.15)(react@19.2.6): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.15 + + use-sync-external-store@1.6.0(react@19.2.6): + dependencies: + react: 19.2.6 + vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0): dependencies: esbuild: 0.27.7 @@ -1622,4 +4677,25 @@ snapshots: jiti: 2.7.0 lightningcss: 1.32.0 + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} + + zustand@4.5.7(@types/react@19.2.15)(react@19.2.6): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + react: 19.2.6 diff --git a/apps/halidoscope/frontend/src-tauri/tauri.conf.json b/apps/halidoscope/frontend/src-tauri/tauri.conf.json index 95215cf66b94..ebba2a7451cc 100644 --- a/apps/halidoscope/frontend/src-tauri/tauri.conf.json +++ b/apps/halidoscope/frontend/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "halidoscope", + "productName": "Halidoscope", "version": "0.1.0", "identifier": "com.halide.halidescope", "build": { @@ -12,9 +12,9 @@ "app": { "windows": [ { - "title": "halidoscope", - "width": 800, - "height": 600 + "title": "Halidoscope", + "width": 1512, + "height": 982 } ], "security": { diff --git a/apps/halidoscope/frontend/src/App.css b/apps/halidoscope/frontend/src/App.css index 65ad14c03397..7a9284afadba 100644 --- a/apps/halidoscope/frontend/src/App.css +++ b/apps/halidoscope/frontend/src/App.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import "@xyflow/react/dist/style.css"; :root { font-family: Inter, Avenir, Helvetica, Arial, sans-serif; @@ -15,3 +16,12 @@ -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } + +@theme { + --color-ps-primary: oklch(0.4423 0 0); + --color-ps-secondary: oklch(0.2768 0 0); + --color-ps-text: oklch(0.8975 0 0); + --color-ps-border-primary: oklch(0.3407 0 0); + --color-ps-border-secondary: oklch(0.3979 0 0); + --color-ps-border-tertiary: oklch(0.4997 0 0); +} diff --git a/apps/halidoscope/frontend/src/App.tsx b/apps/halidoscope/frontend/src/App.tsx index 2192ff576c10..52abfa47d4af 100644 --- a/apps/halidoscope/frontend/src/App.tsx +++ b/apps/halidoscope/frontend/src/App.tsx @@ -1,49 +1,33 @@ import { invoke } from "@tauri-apps/api/core"; import { getMatches } from "@tauri-apps/plugin-cli"; -import { useEffect, useRef, useState } from "react"; - -import FuncCanvas from "./components/FuncCanvas"; -import { Sidebar } from "./components/Sidebar"; +import { ReactFlowProvider } from "@xyflow/react"; +import * as React from "react"; + +import Canvas from "./components/Canvas"; +import Sidebar from "./components/Sidebar"; +import Timeline from "./components/Timeline"; +import { + CanvasRegistry, + CanvasRegistryProvider, +} from "./hooks/canvas-registry"; import { FuncStats } from "./types"; -import { BACKEND_ENDPOINT, WS_ENDPOINT } from "./utils/constants"; +import { loadTracePath, deregisterTrace } from "./utils/api"; import "./App.css"; -async function loadTracePath(path: string, signal: AbortSignal) { - const response = await fetch(`${BACKEND_ENDPOINT}/load-path`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path }), - signal, - }); - - if (!response.ok) { - throw new Error(`Failed to load trace: ${response.statusText}`); - } - - return response.json(); -} - -async function deregisterTrace(session: string) { - const response = await fetch(`${BACKEND_ENDPOINT}/session/${session}`, { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - }); - - if (!response.ok) { - throw new Error(`Failed to deregister trace: ${response.statusText}`); - } - - return response.json(); -} - function App() { - const [session, setSession] = useState(""); - const [funcs, setFuncs] = useState>({}); - const wsRef = useRef(null); - - useEffect(() => { + const [sessionId, setSessionId] = React.useState(""); + const [funcs, setFuncs] = React.useState>({}); + const [dagEdges, setDagEdges] = React.useState>({}); + const [packetCount, setPacketCount] = React.useState(0); + const [canvasRegistry, setCanvasRegistry] = + React.useState(null); + + React.useEffect(() => { const controller = new AbortController(); + // Track the loaded session ID locally so the cleanup closure always has + // the correct value even though sessionId state starts as "". + let loadedSessionId = ""; async function loadTraceFromCLI() { const matches = await getMatches(); @@ -55,24 +39,16 @@ function App() { : `${await invoke("get_cwd")}/${tracePath}`; try { - const { session_id, funcs } = await loadTracePath( - resolved, - controller.signal, - ); + const { session_id, funcs, dag_edges, num_packets } = + await loadTracePath(resolved, controller.signal); - setSession(session_id); + loadedSessionId = session_id; + setSessionId(session_id); setFuncs(funcs); + setDagEdges(dag_edges); + setPacketCount(num_packets); - const ws = new WebSocket(`${WS_ENDPOINT}/ws/${session_id}`); - wsRef.current = ws; - ws.onopen = () => - console.log("WebSocket connected: session=%s", session_id); - ws.onmessage = (event) => - console.log("WebSocket message:", JSON.parse(event.data)); - ws.onerror = (err) => console.error("WebSocket error:", err); - ws.onclose = () => { - wsRef.current = null; - }; + setCanvasRegistry(new CanvasRegistry()); } catch (err) { if ((err as Error).name !== "AbortError") { console.error("Error loading trace from CLI: ", err); @@ -85,43 +61,37 @@ function App() { return () => { controller.abort(); - wsRef.current?.close(); - if (session) { - deregisterTrace(session) - .then((res) => console.log("Successfully removed trace.", res.json())) - .catch((err) => console.error(err)); + + if (loadedSessionId) { + deregisterTrace(loadedSessionId).catch((err) => console.error(err)); } }; }, []); return ( -
- -
- {Object.keys(funcs).length > 0 ? ( - Object.entries(funcs).map(([name, stats]) => ( - - )) - ) : ( -

Loading trace...

- )} -
-
+ +
+ +
+
+ {Object.keys(funcs).length > 0 ? ( + + + + ) : ( +
+

Loading trace...

+
+ )} +
+ +
+
+
); } diff --git a/apps/halidoscope/frontend/src/components/Canvas.tsx b/apps/halidoscope/frontend/src/components/Canvas.tsx new file mode 100644 index 000000000000..275dabfe64b4 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/Canvas.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import { ReactFlow, useViewport } from "@xyflow/react"; + +import FuncCanvas from "./FuncCanvas"; +import { FuncStats } from "../types"; +import { buildEdges, buildNodes, getLayoutedElements } from "../utils/graph"; + +interface CanvasProps { + funcs: Record; + dagEdges: Record; +} + +const NODE_TYPES = { + funcCanvas: FuncCanvas, +}; + +function Canvas({ funcs, dagEdges }: CanvasProps) { + const { nodes, edges } = React.useMemo(() => { + return getLayoutedElements(buildNodes(funcs), buildEdges(dagEdges)); + }, [funcs, dagEdges]); + + const { zoom } = useViewport(); + + return ( +
+ +
+ Zoom: {Math.round(zoom * 100)}% +
+
+ ); +} + +export default Canvas; diff --git a/apps/halidoscope/frontend/src/components/FuncCanvas.tsx b/apps/halidoscope/frontend/src/components/FuncCanvas.tsx index 6db5b6165118..ad1c9b728cac 100644 --- a/apps/halidoscope/frontend/src/components/FuncCanvas.tsx +++ b/apps/halidoscope/frontend/src/components/FuncCanvas.tsx @@ -1,64 +1,63 @@ import * as React from "react"; +import type { Node, NodeProps } from "@xyflow/react"; -interface FuncCanvasProps { - name: string; - width: number; - height: number; - xs: number[]; - ys: number[]; - values: number[]; -} +import { useCanvasRegistry } from "../hooks/canvas-registry"; +import { NodeData } from "../types"; -const MAX_DISPLAY_PX = 800; +type FuncNode = Node; -function FuncCanvas({ name, width, height, xs, ys, values }: FuncCanvasProps) { - const canvas = React.useRef(null); - const scale = Math.min(1, MAX_DISPLAY_PX / width, MAX_DISPLAY_PX / height); +/** + * Renders a single Halide func's store values into a canvas. The canvas owns a + * persistent {@link ImageData} buffer and registers a draw/clear handle with the + * {@link CanvasRegistry}, so incremental range updates accumulate on top of each + * other (forward scrub) until a clear resets it (backward scrub / new trace). + */ +function FuncCanvas({ + id, + data: { name, width, height }, +}: NodeProps) { + const canvasRef = React.useRef(null); + const canvasRegistry = useCanvasRegistry(); React.useEffect(() => { - const ctx = canvas.current?.getContext("2d"); + const ctx = canvasRef.current?.getContext("2d"); + if (!ctx) return; - if (ctx) { - const imageData = ctx.createImageData(width, height); - for (let i = 0; i < imageData.data.length; i += 4) { - imageData.data[i + 3] = 255; - } + const image = ctx.createImageData(width, height); - ctx.putImageData(imageData, 0, 0); - } + const reset = () => { + const data = image.data; + data.fill(0); + // Opaque black background. + for (let i = 3; i < data.length; i += 4) data[i] = 255; + ctx.putImageData(image, 0, 0); + }; - // if (ctx) { - // const imageData = ctx.createImageData(width, height); + reset(); - // for (let i = 0; i < xs.length; i++) { - // const idx = 4 * (ys[i] * width + xs[i]); - // imageData.data[idx + 0] = values[i]; // R - // imageData.data[idx + 1] = values[i]; // G - // imageData.data[idx + 2] = values[i]; // B - // imageData.data[idx + 3] = 255; - // } + const unregister = canvasRegistry.register(id, { + draw: ({ xs, ys, values }) => { + const data = image.data; + for (let i = 0; i < xs.length; i++) { + const idx = 4 * (ys[i] * width + xs[i]); + const v = values[i]; + data[idx] = v; + data[idx + 1] = v; + data[idx + 2] = v; + data[idx + 3] = 255; + } + ctx.putImageData(image, 0, 0); + }, + clear: reset, + }); - // ctx.putImageData(imageData, 0, 0); - // } - }, [xs, ys, values, width, height]); + return unregister; + }, [id, width, height, canvasRegistry]); return (
- - {name} - {scale < 1 && ( - - {Math.round(scale * 100)}% - - )} - - + {name} +
); } diff --git a/apps/halidoscope/frontend/src/components/Sidebar.tsx b/apps/halidoscope/frontend/src/components/Sidebar.tsx index 1520851cf6e2..58515d039a3c 100644 --- a/apps/halidoscope/frontend/src/components/Sidebar.tsx +++ b/apps/halidoscope/frontend/src/components/Sidebar.tsx @@ -4,19 +4,25 @@ interface SidebarProps { funcs: Record; } -export function Sidebar({ funcs }: SidebarProps) { +function Sidebar({ funcs }: SidebarProps) { return ( - +
+ +
+
+
); } + +export default Sidebar; diff --git a/apps/halidoscope/frontend/src/components/Timeline.tsx b/apps/halidoscope/frontend/src/components/Timeline.tsx new file mode 100644 index 000000000000..04f53a1e5690 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/Timeline.tsx @@ -0,0 +1,254 @@ +import * as d3 from "d3"; +import { Slider } from "radix-ui"; +import * as React from "react"; + +import { CanvasRegistry } from "../hooks/canvas-registry"; +import { RangeRequest, RenderResponse } from "../types"; +import { + PLAYBACK_INTERVAL_MS, + PLAYBACK_STEP, + SCRUB_DEBOUNCE_MS, + WS_ENDPOINT, +} from "../utils/constants"; + +interface TimelineProps { + packetCount: number; + sessionId: string; + canvasRegistry: CanvasRegistry | null; +} + +function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { + // Track the current packet index. + const [packetIndex, setPacketIndex] = React.useState(0); + const [playing, setPlaying] = React.useState(false); + const wsRef = React.useRef(null); + + // Use refs to synchronously track mutable state without triggering re-renders. + const inFlightRef = React.useRef(false); + const timeRef = React.useRef(0); + const renderedEndRef = React.useRef(0); + const pendingEndRef = React.useRef(0); + const scrubTimerRef = React.useRef(null); + + // Send the next range needed to bring the canvases to the current playhead. + // Only one request is in flight at a time; on each response we pump again so + // the canvases catch up to wherever the playhead has moved. + const pump = React.useCallback(() => { + if ( + inFlightRef.current || + !canvasRegistry || + !wsRef.current || + wsRef.current.readyState !== WebSocket.OPEN + ) { + console.warn("Skipping pump: ", { + inFlight: inFlightRef.current, + hasRegistry: !!canvasRegistry, + wsRef: wsRef.current, + readyState: wsRef.current?.readyState, + }); + return; + } + + const targetEnd = timeRef.current + 1; + const rendered = renderedEndRef.current; + + if (targetEnd === rendered) { + return; + } + + let start: number; + if (targetEnd < rendered) { + // Backward: discard accumulated pixels and re-render from the start. + canvasRegistry.clearAll(); + renderedEndRef.current = 0; + start = 0; + } else { + // Forward: render only the new delta on top of the existing buffers. + start = rendered; + } + + // Send the request over the WebSocket. + inFlightRef.current = true; + pendingEndRef.current = targetEnd; + const request: RangeRequest = { start, end: targetEnd }; + wsRef.current.send(JSON.stringify(request)); + }, [canvasRegistry]); + + // Scrub: track the playhead immediately, but defer rendering until the slider + // settles so a drag doesn't fire a request per intermediate position. + const onScrub = React.useCallback( + (next: number) => { + timeRef.current = next; + setPacketIndex(next); + + if (scrubTimerRef.current !== null) { + window.clearTimeout(scrubTimerRef.current); + } + + scrubTimerRef.current = window.setTimeout(() => { + scrubTimerRef.current = null; + pump(); + }, SCRUB_DEBOUNCE_MS); + }, + [pump], + ); + + const onTogglePlay = React.useCallback(() => { + // Starting from the end replays from the beginning. + if (!playing && timeRef.current >= packetCount - 1) { + timeRef.current = 0; + setPacketIndex(0); + } + + setPlaying((p) => !p); + }, [playing, packetCount]); + + React.useEffect(() => { + if (!sessionId || !canvasRegistry) { + return; + } + + wsRef.current = new WebSocket(`${WS_ENDPOINT}/ws/${sessionId}`); + + wsRef.current.onopen = () => { + pump(); + }; + + wsRef.current.onmessage = (event: MessageEvent) => { + const res: RenderResponse = JSON.parse(event.data); + if (canvasRegistry) { + for (const update of res.updates) { + canvasRegistry.dispatch(update); + } + } + + if (res.done) { + renderedEndRef.current = pendingEndRef.current; + inFlightRef.current = false; + pump(); + } + }; + + wsRef.current.onerror = (err: Event) => + console.error("WebSocket error:", err); + wsRef.current.onclose = () => {}; + + return () => { + // Reset inFlight so a torn-down socket can't leave the next connection + // permanently blocked on the guard in pump. + inFlightRef.current = false; + if (wsRef.current) { + wsRef.current.close(); + wsRef.current.onopen = null; + wsRef.current.onmessage = null; + wsRef.current.onerror = null; + wsRef.current.onclose = null; + wsRef.current = null; + } + }; + }, [pump, canvasRegistry, sessionId]); + + // Playback loop: advance the playhead on a fixed interval and pump after each + // step. Rendering may lag the playhead on large traces; it catches up via the + // pump-on-response in the WebSocket handler. + React.useEffect(() => { + if (!playing) { + return; + } + + const id = window.setInterval(() => { + const next = Math.min(timeRef.current + PLAYBACK_STEP, packetCount - 1); + timeRef.current = next; + setPacketIndex(next); + pump(); + + if (next >= packetCount - 1) { + setPlaying(false); + } + }, PLAYBACK_INTERVAL_MS); + + return () => window.clearInterval(id); + }, [packetCount, playing, pump]); + + const disabled = packetCount <= 0; + const ticks = d3 + .ticks(0, packetCount - 1, 10) + .filter((t) => t > 0 && t < packetCount - 1); + + return ( +
+
+ +
+ {ticks.map((tick) => ( +
+

+ {d3.format(".2s")(tick)} +

+
+ ))} + onScrub(values[0])} + value={[packetIndex]} + disabled={disabled} + > + + + + + +
+
+
+ Packets + + {packetIndex.toLocaleString()} /{" "} + {Math.max(packetCount - 1, 0).toLocaleString()} + +
+
+ ); +} + +export default Timeline; diff --git a/apps/halidoscope/frontend/src/hooks/canvas-registry.ts b/apps/halidoscope/frontend/src/hooks/canvas-registry.ts new file mode 100644 index 000000000000..8fbc8fc6e7a4 --- /dev/null +++ b/apps/halidoscope/frontend/src/hooks/canvas-registry.ts @@ -0,0 +1,93 @@ +import * as React from "react"; + +import { FuncUpdate } from "../types"; + +/** + * The imperative draw surface a {@link FuncCanvas} exposes to the bus. The + * canvas owns its persistent pixel buffer so that store writes accumulate + * across incremental range updates. + */ +export interface CanvasHandle { + /** Apply a range's pixel writes on top of the existing buffer. */ + draw: (update: FuncUpdate) => void; + /** Reset the buffer to empty (used on backward scrubs / new traces). */ + clear: () => void; +} + +/** + * Routes backend {@link FuncUpdate}s to the matching {@link FuncCanvas} without + * pushing pixel data through React state. Canvases register a {@link CanvasHandle} + * keyed by their qualified func name (the node id); the registry resolves a + * packet's raw func name to that key using the same matching the backend uses. + */ +export class CanvasRegistry { + private handlers = new Map(); + // raw packet func name -> resolved qualified handler key (or null if none). + private resolveCache = new Map(); + + /** Register a canvas under its qualified func name; returns an unregister fn. */ + register(qualifiedName: string, handle: CanvasHandle): () => void { + this.handlers.set(qualifiedName, handle); + this.resolveCache.clear(); + + return () => { + if (this.handlers.get(qualifiedName) === handle) { + this.handlers.delete(qualifiedName); + this.resolveCache.clear(); + } + }; + } + + /** + * Resolve a raw func name (e.g. "f0") to a registered qualified key (e.g. + * "local_laplacian:f0"). Mirrors the backend's `_get_func_item_for_packet`: + * exact match, then a "pipeline:func" suffix, then a substring fallback. + */ + private resolve(raw: string): string | null { + const cached = this.resolveCache.get(raw); + if (cached !== undefined) return cached; + + let match: string | null = null; + if (this.handlers.has(raw)) { + match = raw; + } else { + for (const name of this.handlers.keys()) { + if (name.endsWith(`:${raw}`) || name.includes(raw)) { + match = name; + break; + } + } + } + + this.resolveCache.set(raw, match); + + return match; + } + + /** Route one func's updates to its canvas, if a matching one is registered. */ + dispatch(update: FuncUpdate): void { + const key = this.resolve(update.func); + if (key) this.handlers.get(key)?.draw(update); + } + + /** Clear every registered canvas (backward scrub / re-render from scratch). */ + clearAll(): void { + for (const handle of this.handlers.values()) handle.clear(); + } +} + +const CanvasRegistryContext = React.createContext(null); +export const CanvasRegistryProvider = CanvasRegistryContext.Provider; + +/** Access the {@link CanvasRegistry} provided by an ancestor {@link CanvasRegistryProvider}. */ +export function useCanvasRegistry(): CanvasRegistry { + const bus = React.useContext(CanvasRegistryContext); + + if (!bus) { + throw new Error( + "useCanvasRegistry must be used within a CanvasRegistryProvider", + ); + } + + return bus; +} diff --git a/apps/halidoscope/frontend/src/types/index.ts b/apps/halidoscope/frontend/src/types/index.ts index bdc69664243b..ef1eb17349c7 100644 --- a/apps/halidoscope/frontend/src/types/index.ts +++ b/apps/halidoscope/frontend/src/types/index.ts @@ -5,3 +5,35 @@ export interface FuncStats { min_value: number; max_value: number; } + +export interface NodeData extends Record, FuncStats { + width: number; + height: number; +} + +/** + * A single func's pixel updates for a rendered range, as returned by the + * backend WebSocket. `values` are pre-normalized to 0-255 grayscale, and + * `xs`/`ys` are coordinates within the func's bounding box. `func` is the raw + * (unqualified) Halide func name. + */ +export interface FuncUpdate { + func: string; + xs: number[]; + ys: number[]; + values: number[]; +} + +/** A range request sent to the backend WebSocket. Renders stores in [start, end). */ +export interface RangeRequest { + start: number; + end: number; +} + +/** The backend's response to a {@link RangeRequest}. */ +export interface RenderResponse { + updates: FuncUpdate[]; + done: boolean; + start: number; + end: number; +} diff --git a/apps/halidoscope/frontend/src/utils/api.ts b/apps/halidoscope/frontend/src/utils/api.ts new file mode 100644 index 000000000000..86d629fe6a9b --- /dev/null +++ b/apps/halidoscope/frontend/src/utils/api.ts @@ -0,0 +1,61 @@ +import { BACKEND_ENDPOINT } from "./constants"; + +import type { FuncStats } from "../types"; + +interface LoadTraceResponse { + session_id: string; + funcs: Record; + dag_edges: Record; + num_packets: number; +} + +/** + * Load a Halide trace from the specified path on disk. + * + * @param path The absolute path to the trace file on disk. Passed to the + * --trace CLI argument. If a relative path is provided, the calling code is + * responsible for resolving it against the current working directory. + * @param signal An {@link AbortSignal} to cancel the request. + * @returns An {@link LoadTraceResponse} containing the session ID, func stats, + * DAG edges, and total number of packets in the trace. + * @throws An error if the request fails or the response is not OK. + */ +export async function loadTracePath( + path: string, + signal: AbortSignal, +): Promise { + const response = await fetch(`${BACKEND_ENDPOINT}/load-path`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path }), + signal, + }); + + if (!response.ok) { + throw new Error(`Failed to load trace: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Deregister a trace session on the backend, freeing associated resources. + * + * @param session The session ID to deregister. + * @returns A promise resolving to the JSON response from the backend. + * @throws An error if the request fails or the response is not OK. + */ +export async function deregisterTrace( + session: string, +): Promise<{ deleted: string }> { + const response = await fetch(`${BACKEND_ENDPOINT}/session/${session}`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }); + + if (!response.ok) { + throw new Error(`Failed to deregister trace: ${response.statusText}`); + } + + return response.json(); +} diff --git a/apps/halidoscope/frontend/src/utils/constants.ts b/apps/halidoscope/frontend/src/utils/constants.ts index fd694eeebb07..23a2b12e10e3 100644 --- a/apps/halidoscope/frontend/src/utils/constants.ts +++ b/apps/halidoscope/frontend/src/utils/constants.ts @@ -1,2 +1,9 @@ export const BACKEND_ENDPOINT = "http://localhost:8765"; export const WS_ENDPOINT = "ws://localhost:8765"; + +/** Packets advanced per playback tick (mirrors neotrace's playback step). */ +export const PLAYBACK_STEP = 10000; +/** Playback tick interval in ms (~33fps, mirrors neotrace). */ +export const PLAYBACK_INTERVAL_MS = 30; +/** Debounce window in ms before a settled scrub position is rendered. */ +export const SCRUB_DEBOUNCE_MS = 50; diff --git a/apps/halidoscope/frontend/src/utils/func.ts b/apps/halidoscope/frontend/src/utils/func.ts new file mode 100644 index 000000000000..31cc0d9ac155 --- /dev/null +++ b/apps/halidoscope/frontend/src/utils/func.ts @@ -0,0 +1,18 @@ +import type { FuncStats } from "../types"; + +/** + * Compute the width and height of a function's bounding box based on its min + * and max coordinates. + * + * @param stats The {@link FuncStats} of the Halide func. + * @returns The computed width and height of the func's bounding box. + */ +export function computeFuncSize(stats: FuncStats): { + width: number; + height: number; +} { + const width = (stats.max_coords[0] ?? 0) - (stats.min_coords[0] ?? 0) || 1; + const height = (stats.max_coords[1] ?? 0) - (stats.min_coords[1] ?? 0) || 1; + + return { width, height }; +} diff --git a/apps/halidoscope/frontend/src/utils/graph.ts b/apps/halidoscope/frontend/src/utils/graph.ts new file mode 100644 index 000000000000..50b28b8e9f71 --- /dev/null +++ b/apps/halidoscope/frontend/src/utils/graph.ts @@ -0,0 +1,95 @@ +import Dagre from "@dagrejs/dagre"; +import type { Node, Edge } from "@xyflow/react"; + +import { FuncStats, NodeData } from "../types"; +import { computeFuncSize } from "./func"; + +/** + * Build xyflow nodes from the backend's funcs payload, which maps Halide func + * @param funcs + * @returns + */ +export function buildNodes(funcs: Record): Node[] { + return Object.entries(funcs).map(([name, stats]) => { + const { width, height } = computeFuncSize(stats); + + return { + id: name, + type: "funcCanvas", + position: { + x: 0, + y: 0, + }, + data: { + ...stats, + width, + height, + }, + style: { + width, + height, + }, + }; + }); +} + +/** + * Build xyflow edges from the backend's dag_edges payload, which maps Halide + * consumers to their producers. + * + * @param dagEdges The dag_edges payload from the backend. + * @returns An array of edges formatted for use with xyflow, where each edge has an id, source, and target. + */ +export function buildEdges(dagEdges: Record): Edge[] { + const edges: { id: string; source: string; target: string }[] = []; + + for (const [producer, consumers] of Object.entries(dagEdges)) { + for (const consumer of consumers) { + edges.push({ + id: `${producer}-${consumer}`, + source: producer, + target: consumer, + }); + } + } + + return edges; +} + +export function getLayoutedElements( + nodes: Node[], + edges: Edge[], +): { nodes: Node[]; edges: Edge[] } { + const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: 80 }); + + edges.forEach((edge) => g.setEdge(edge.source, edge.target)); + nodes.forEach((node) => { + const width = + node.measured?.width ?? (node.style?.width as number | undefined) ?? 150; + const height = + node.measured?.height ?? (node.style?.height as number | undefined) ?? 50; + g.setNode(node.id, { ...node, width, height }); + }); + + Dagre.layout(g); + + return { + nodes: nodes.map((node) => { + const position = g.node(node.id); + const width = + node.measured?.width ?? + (node.style?.width as number | undefined) ?? + 150; + const height = + node.measured?.height ?? + (node.style?.height as number | undefined) ?? + 50; + const x = position.x - width / 2; + const y = position.y - height / 2; + + return { ...node, position: { x, y } }; + }), + edges, + }; +} From 7aa94378a64f614d305eb9dedcfc6219839fbd42 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 9 Jun 2026 09:48:28 -0700 Subject: [PATCH 09/67] feat: Support RGB. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/backend/backend/main.py | 41 ++++++++++--- apps/halidoscope/frontend/src/App.css | 8 +++ .../frontend/src/components/Canvas.tsx | 6 +- .../frontend/src/components/FuncCanvas.tsx | 57 +++++++++++++------ .../frontend/src/components/Timeline.tsx | 4 +- apps/halidoscope/frontend/src/types/index.ts | 30 +++++++--- 6 files changed, 110 insertions(+), 36 deletions(-) diff --git a/apps/halidoscope/backend/backend/main.py b/apps/halidoscope/backend/backend/main.py index afe00b0fd296..47106bfe79dd 100644 --- a/apps/halidoscope/backend/backend/main.py +++ b/apps/halidoscope/backend/backend/main.py @@ -16,7 +16,7 @@ log = logging.getLogger(__name__) -app = FastAPI(title="halide-viz backend") +app = FastAPI(title="Halidoscope Backend") origins = ["http://localhost:1420"] app.add_middleware( @@ -91,7 +91,7 @@ def _get_func_item_for_packet(session_id: str, func_name: str) -> Any: funcs = _sessions[session_id]["funcs"] for name, stats in funcs.items(): - if func_name in name or name.endswith(f":{func_name}"): + if name == func_name or name.endswith(f":{func_name}"): cache[func_name] = stats return stats @@ -103,7 +103,7 @@ def _render_range(session_id: str, start: int, end: int) -> list[dict[str, Any]] packets = _packets[session_id] store_indices = _store_indices[session_id] - # pending: func_name -> [px_list, py_list, val_list, func_stats] + # pending: func_name -> [px_list, py_list, c_list, val_list, func_stats] pending: dict[str, list] = {} end = min(end, len(packets)) @@ -129,9 +129,9 @@ def _render_range(session_id: str, start: int, end: int) -> list[dict[str, Any]] min_x = min_coords[0] if min_coords else 0 min_y = min_coords[1] if len(min_coords) > 1 else 0 - if packet.func not in pending: - pending[packet.func] = [[], [], [], func_stats] - px_list, py_list, val_list, _ = pending[packet.func] + if func_stats["name"] not in pending: + pending[func_stats["name"]] = [[], [], [], [], func_stats] + px_list, py_list, c_list, val_list, _ = pending[func_stats["name"]] for lane in range(n_lanes): if dims_per_lane >= 2: @@ -143,13 +143,19 @@ def _render_range(session_id: str, start: int, end: int) -> list[dict[str, Any]] else: x = -min_x y = -min_y + c = ( + coords[2 * n_lanes + lane] + if dims_per_lane >= 3 and 2 * n_lanes + lane < len(coords) + else -1 + ) if lane < len(values): px_list.append(x) py_list.append(y) + c_list.append(c) val_list.append(values[lane]) updates = [] - for func_name, (px_list, py_list, val_list, func_stats) in pending.items(): + for func_name, (px_list, py_list, c_list, val_list, func_stats) in pending.items(): if not px_list: continue @@ -182,7 +188,26 @@ def _render_range(session_id: str, start: int, end: int) -> list[dict[str, Any]] ys = ys[mask] normalized = normalized[mask] - if len(xs): + is_color = ( + len(min_coords) >= 3 + and len(max_coords) >= 3 + and max_coords[2] - min_coords[2] >= 3 + ) + + if is_color: + cs = np.asarray(c_list, dtype=np.intp)[mask] + update: dict[str, Any] = {"func": func_name} + for ch_idx, key in [(0, "r"), (1, "g"), (2, "b")]: + m = cs == ch_idx + if m.any(): + update[key] = { + "xs": xs[m].tolist(), + "ys": ys[m].tolist(), + "values": normalized[m].tolist(), + } + if len(update) > 1: + updates.append(update) + elif len(xs): updates.append( { "func": func_name, diff --git a/apps/halidoscope/frontend/src/App.css b/apps/halidoscope/frontend/src/App.css index 7a9284afadba..101266e72787 100644 --- a/apps/halidoscope/frontend/src/App.css +++ b/apps/halidoscope/frontend/src/App.css @@ -15,6 +15,8 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; + + --zoom-level: 1; } @theme { @@ -25,3 +27,9 @@ --color-ps-border-secondary: oklch(0.3979 0 0); --color-ps-border-tertiary: oklch(0.4997 0 0); } + +@layer components { + .text-responsive { + font-size: clamp(0.5rem, calc(1rem / var(--zoom-level)), 1.5rem); + } +} diff --git a/apps/halidoscope/frontend/src/components/Canvas.tsx b/apps/halidoscope/frontend/src/components/Canvas.tsx index 275dabfe64b4..ede6145c38f6 100644 --- a/apps/halidoscope/frontend/src/components/Canvas.tsx +++ b/apps/halidoscope/frontend/src/components/Canvas.tsx @@ -21,6 +21,10 @@ function Canvas({ funcs, dagEdges }: CanvasProps) { const { zoom } = useViewport(); + React.useEffect(() => { + document.documentElement.style.setProperty("--zoom-level", zoom.toString()); + }, [zoom]); + return (
-
+
Zoom: {Math.round(zoom * 100)}%
diff --git a/apps/halidoscope/frontend/src/components/FuncCanvas.tsx b/apps/halidoscope/frontend/src/components/FuncCanvas.tsx index ad1c9b728cac..88dd44a641de 100644 --- a/apps/halidoscope/frontend/src/components/FuncCanvas.tsx +++ b/apps/halidoscope/frontend/src/components/FuncCanvas.tsx @@ -2,16 +2,10 @@ import * as React from "react"; import type { Node, NodeProps } from "@xyflow/react"; import { useCanvasRegistry } from "../hooks/canvas-registry"; -import { NodeData } from "../types"; +import { NodeData, ChannelData } from "../types"; type FuncNode = Node; -/** - * Renders a single Halide func's store values into a canvas. The canvas owns a - * persistent {@link ImageData} buffer and registers a draw/clear handle with the - * {@link CanvasRegistry}, so incremental range updates accumulate on top of each - * other (forward scrub) until a clear resets it (backward scrub / new trace). - */ function FuncCanvas({ id, data: { name, width, height }, @@ -19,6 +13,15 @@ function FuncCanvas({ const canvasRef = React.useRef(null); const canvasRegistry = useCanvasRegistry(); + const applyChannel = React.useCallback( + (imageData: ImageDataArray, ch: ChannelData, offset: number) => { + for (let i = 0; i < ch.xs.length; i++) { + imageData[4 * (ch.ys[i] * width + ch.xs[i]) + offset] = ch.values[i]; + } + }, + [width], + ); + React.useEffect(() => { const ctx = canvasRef.current?.getContext("2d"); if (!ctx) return; @@ -36,27 +39,45 @@ function FuncCanvas({ reset(); const unregister = canvasRegistry.register(id, { - draw: ({ xs, ys, values }) => { + draw: ({ xs, ys, values, r, g, b }) => { const data = image.data; - for (let i = 0; i < xs.length; i++) { - const idx = 4 * (ys[i] * width + xs[i]); - const v = values[i]; - data[idx] = v; - data[idx + 1] = v; - data[idx + 2] = v; - data[idx + 3] = 255; + + if (r) { + applyChannel(data, r, 0); + } + + if (g) { + applyChannel(data, g, 1); } + + if (b) { + applyChannel(data, b, 2); + } + + if (xs && ys && values) { + for (let i = 0; i < xs.length; i++) { + const idx = 4 * (ys[i] * width + xs[i]); + const v = values[i]; + data[idx] = v; + data[idx + 1] = v; + data[idx + 2] = v; + data[idx + 3] = 255; + } + } + ctx.putImageData(image, 0, 0); }, clear: reset, }); return unregister; - }, [id, width, height, canvasRegistry]); + }, [id, width, height, canvasRegistry, applyChannel]); return ( -
- {name} +
+ + {name} +
); diff --git a/apps/halidoscope/frontend/src/components/Timeline.tsx b/apps/halidoscope/frontend/src/components/Timeline.tsx index 04f53a1e5690..e2bd79a61dfd 100644 --- a/apps/halidoscope/frontend/src/components/Timeline.tsx +++ b/apps/halidoscope/frontend/src/components/Timeline.tsx @@ -225,7 +225,7 @@ function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { className="relative top-1/2 -translate-y-1/2 flex items-center select-none touch-none w-full h-5" defaultValue={[0]} max={packetCount - 1} - step={1} + step={10000} onValueChange={(values) => onScrub(values[0])} value={[packetIndex]} disabled={disabled} @@ -234,7 +234,7 @@ function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { diff --git a/apps/halidoscope/frontend/src/types/index.ts b/apps/halidoscope/frontend/src/types/index.ts index ef1eb17349c7..46270724469a 100644 --- a/apps/halidoscope/frontend/src/types/index.ts +++ b/apps/halidoscope/frontend/src/types/index.ts @@ -11,17 +11,33 @@ export interface NodeData extends Record, FuncStats { height: number; } +/** Per-channel pixel writes for one color channel of a color func. */ +export interface ChannelData { + xs: number[]; + ys: number[]; + values: number[]; +} + /** - * A single func's pixel updates for a rendered range, as returned by the - * backend WebSocket. `values` are pre-normalized to 0-255 grayscale, and - * `xs`/`ys` are coordinates within the func's bounding box. `func` is the raw - * (unqualified) Halide func name. + * A single Func's pixel updates for a rendered range, as returned by the + * backend. + * + * @property func The name of the Func being updated. + * @property xs The x-coordinates of the updated pixels. + * @property ys The y-coordinates of the updated pixels. + * @property values Normalized 0-255 values for the updated pixels (Grayscale). + * @property r The red channel for the updated pixels. + * @property g The green channel for the updated pixels. + * @property b The blue channel for the updated pixels. */ export interface FuncUpdate { func: string; - xs: number[]; - ys: number[]; - values: number[]; + xs?: number[]; + ys?: number[]; + values?: number[]; + r?: ChannelData; + g?: ChannelData; + b?: ChannelData; } /** A range request sent to the backend WebSocket. Renders stores in [start, end). */ From 3ab8906a9ea6693a02624d71d5646d3f3a70dab8 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 16 Jun 2026 12:07:00 -0700 Subject: [PATCH 10/67] feat: Add support for heatmap visualizations of load / store counts. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/backend/backend/main.py | 351 ++++++++-- apps/halidoscope/frontend/eslint.config.mjs | 6 + apps/halidoscope/frontend/package.json | 3 +- apps/halidoscope/frontend/pnpm-lock.yaml | 628 +++++------------- apps/halidoscope/frontend/src/App.css | 33 +- apps/halidoscope/frontend/src/App.tsx | 67 +- .../frontend/src/components/Canvas.tsx | 46 -- .../frontend/src/components/FuncCanvas.tsx | 86 --- .../frontend/src/components/Sidebar.tsx | 28 - .../src/components/controls/ControlPanel.tsx | 39 ++ .../src/components/controls/ControlTabs.tsx | 41 ++ .../components/controls/funcs/FuncsPanel.tsx | 86 +++ .../controls/playback/PlaybackLegend.tsx | 81 +++ .../controls/playback/PlaybackPanel.tsx | 53 ++ .../frontend/src/components/shared/Canvas.tsx | 84 +++ .../src/components/shared/HandleCircle.tsx | 17 + .../src/components/views/ViewTabs.tsx | 26 + .../components/views/tracer/FuncCanvas.tsx | 248 +++++++ .../src/components/views/tracer/FuncEdge.tsx | 18 + .../src/components/views/tracer/Tracer.tsx | 42 ++ .../tracer/TracerTimeline.tsx} | 53 +- apps/halidoscope/frontend/src/hooks/trace.ts | 25 + apps/halidoscope/frontend/src/state/func.ts | 3 + .../frontend/src/state/playback.ts | 5 + apps/halidoscope/frontend/src/types/index.ts | 19 +- apps/halidoscope/frontend/src/utils/api.ts | 2 + apps/halidoscope/frontend/src/utils/func.ts | 18 - apps/halidoscope/frontend/src/utils/graph.ts | 26 +- apps/halidoscope/frontend/tsconfig.json | 7 +- apps/halidoscope/frontend/vite.config.ts | 4 +- .../src/halide/halide_/PyTrace.cpp | 125 +++- 31 files changed, 1496 insertions(+), 774 deletions(-) delete mode 100644 apps/halidoscope/frontend/src/components/Canvas.tsx delete mode 100644 apps/halidoscope/frontend/src/components/FuncCanvas.tsx delete mode 100644 apps/halidoscope/frontend/src/components/Sidebar.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/playback/PlaybackLegend.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/playback/PlaybackPanel.tsx create mode 100644 apps/halidoscope/frontend/src/components/shared/Canvas.tsx create mode 100644 apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx create mode 100644 apps/halidoscope/frontend/src/components/views/ViewTabs.tsx create mode 100644 apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx create mode 100644 apps/halidoscope/frontend/src/components/views/tracer/FuncEdge.tsx create mode 100644 apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx rename apps/halidoscope/frontend/src/components/{Timeline.tsx => views/tracer/TracerTimeline.tsx} (85%) create mode 100644 apps/halidoscope/frontend/src/hooks/trace.ts create mode 100644 apps/halidoscope/frontend/src/state/func.ts create mode 100644 apps/halidoscope/frontend/src/state/playback.ts delete mode 100644 apps/halidoscope/frontend/src/utils/func.ts diff --git a/apps/halidoscope/backend/backend/main.py b/apps/halidoscope/backend/backend/main.py index 47106bfe79dd..d019eec93480 100644 --- a/apps/halidoscope/backend/backend/main.py +++ b/apps/halidoscope/backend/backend/main.py @@ -1,6 +1,5 @@ from __future__ import annotations -import bisect import logging import time as _time import uuid @@ -8,7 +7,7 @@ import numpy as np import uvicorn -from fastapi import FastAPI, HTTPException, UploadFile +from fastapi import FastAPI, HTTPException from pydantic import BaseModel from fastapi.middleware.cors import CORSMiddleware from fastapi.websockets import WebSocket, WebSocketDisconnect @@ -30,43 +29,86 @@ _sessions: dict[str, Any] = {} _packets: dict[str, Any] = {} _store_indices: dict[str, list[int]] = {} +_load_indices: dict[str, list[int]] = {} _func_name_cache: dict[ str, dict[str, Any | None] ] = {} # session_id -> {packet_func -> stats | None} -def _serialize_func_stats(stats: FuncStats) -> dict[str, Any]: - return { - "name": stats.name, - "min_coords": list(stats.min_coords), - "max_coords": list(stats.max_coords), - "min_value": stats.min_value, - "max_value": stats.max_value, - } +def _analyze_packets( + session_id: str, trace: Any, funcs: dict[str, FuncStats] +) -> dict[str, Any]: + # Delegate the packet scan to C++ for performance; the result maps each + # qualified func name to its per-func max store and load counts. + max_counts = trace.compute_max_load_store_counts() + + result: dict[str, Any] = {} + global_max_store = 0 + global_max_load = 0 + + for func_name, func_stats in funcs.items(): + counts = max_counts.get(func_name) + if counts is None: + continue + + max_store: int = counts["max_store_count"] + max_load: int = counts["max_load_count"] + + min_coords = list(func_stats.min_coords) + max_coords = list(func_stats.max_coords) + width = max_coords[0] - min_coords[0] + height = ( + max_coords[1] - min_coords[1] + if len(min_coords) > 1 and len(max_coords) > 1 + else 1 + ) + + entry: dict[str, Any] = { + "name": func_name, + "width": width, + "height": height, + "min_coords": min_coords, + "max_coords": max_coords, + "min_value": func_stats.min_value, + "max_value": func_stats.max_value, + "max_store_count": max_store, + "max_load_count": max_load, + } + result[func_name] = entry + _func_name_cache[session_id][func_name] = entry + + global_max_store = max(global_max_store, max_store) + global_max_load = max(global_max_load, max_load) + + return result, global_max_store, global_max_load def _register_trace(trace: Any) -> dict[str, Any]: session_id = str(uuid.uuid4()) + + # Cache once; avoids full C++ vector copy on each access. + _packets[session_id] = trace.packets + _store_indices[session_id] = np.array(trace.store_indices()) + _load_indices[session_id] = np.array(trace.load_indices()) + _func_name_cache[session_id] = {} + + # Analyze packets to compute load/store counts and other stats per Func. + funcs, global_max_store, global_max_load = _analyze_packets( + session_id, trace, trace.funcs + ) + payload = { "session_id": session_id, "num_packets": len(trace), - "funcs": {name: _serialize_func_stats(s) for name, s in trace.funcs.items()}, + "funcs": funcs, "dag_edges": {k: list(v) for k, v in trace.dag_edges.items()}, "pipelines": {str(k): v for k, v in trace.pipelines.items()}, + "global_max_store_count": global_max_store, + "global_max_load_count": global_max_load, } _sessions[session_id] = payload - # Cache once; avoids full C++ vector copy each access in render_ws. - _packets[session_id] = trace.packets - _store_indices[session_id] = list(trace.store_indices()) - _func_name_cache[session_id] = {} - return payload - -@app.post("/load") -async def load_trace(file: UploadFile) -> dict[str, Any]: - data = await file.read() - trace = Trace.load_bytes(bytes(data)) - return _register_trace(trace) + return payload class LoadPathRequest(BaseModel): @@ -80,6 +122,7 @@ async def load_trace_path(request: LoadPathRequest) -> dict[str, Any]: data = f.read() except OSError as e: raise HTTPException(status_code=400, detail=str(e)) + trace = Trace.load_bytes(data) return _register_trace(trace) @@ -101,58 +144,51 @@ def _get_func_item_for_packet(session_id: str, func_name: str) -> Any: def _render_range(session_id: str, start: int, end: int) -> list[dict[str, Any]]: packets = _packets[session_id] - store_indices = _store_indices[session_id] + indices = _store_indices[session_id] # pending: func_name -> [px_list, py_list, c_list, val_list, func_stats] pending: dict[str, list] = {} end = min(end, len(packets)) - lo = bisect.bisect_left(store_indices, start) - hi = bisect.bisect_right(store_indices, end - 1) + lo = np.searchsorted(indices, start, side="left") + hi = np.searchsorted(indices, end - 1, side="right") for si in range(lo, hi): - i = store_indices[si] - packet = packets[i] + # Grab the next load or store packet in the requested range. + packet = packets[indices[si]] func_stats = _get_func_item_for_packet(session_id, packet.func) - if func_stats is None: - continue - - values = packet.get_values() - if not values: - continue - - coords = packet.coordinates + coords = np.asarray(packet.coordinates) + values_arr = np.asarray(packet.get_values()) n_lanes = packet.type_lanes - dims_per_lane = len(coords) // n_lanes if n_lanes > 0 else len(coords) + dims_per_lane = len(coords) // n_lanes min_coords = func_stats["min_coords"] min_x = min_coords[0] if min_coords else 0 min_y = min_coords[1] if len(min_coords) > 1 else 0 + n = min(n_lanes, len(values_arr)) + # Check to see if there are pending updates for this Func; if not, + # initialize the lists and cache the func_stats. if func_stats["name"] not in pending: pending[func_stats["name"]] = [[], [], [], [], func_stats] px_list, py_list, c_list, val_list, _ = pending[func_stats["name"]] - for lane in range(n_lanes): - if dims_per_lane >= 2: - x = coords[lane] - min_x - y = coords[n_lanes + lane] - min_y - elif dims_per_lane == 1: - x = coords[lane] - min_x - y = -min_y - else: - x = -min_x - y = -min_y - c = ( - coords[2 * n_lanes + lane] - if dims_per_lane >= 3 and 2 * n_lanes + lane < len(coords) - else -1 - ) - if lane < len(values): - px_list.append(x) - py_list.append(y) - c_list.append(c) - val_list.append(values[lane]) + xs = coords[:n_lanes] - min_x + ys = ( + coords[n_lanes : 2 * n_lanes] - min_y + if dims_per_lane >= 2 + else np.full(n_lanes, -min_y, dtype=np.intp) + ) + cs = ( + coords[2 * n_lanes : 3 * n_lanes] + if dims_per_lane >= 3 + else np.full(n_lanes, -1, dtype=np.intp) + ) + + px_list.extend(xs[:n].tolist()) + py_list.extend(ys[:n].tolist()) + c_list.extend(cs[:n].tolist()) + val_list.extend(values_arr[:n].tolist()) updates = [] for func_name, (px_list, py_list, c_list, val_list, func_stats) in pending.items(): @@ -174,9 +210,7 @@ def _render_range(session_id: str, start: int, end: int) -> list[dict[str, Any]] min_coords = func_stats["min_coords"] max_coords = func_stats["max_coords"] - width = ( - max(1, max_coords[0] - min_coords[0]) if min_coords and max_coords else 1 - ) + width = max_coords[0] - min_coords[0] height = ( max(1, max_coords[1] - min_coords[1]) if len(min_coords) > 1 and len(max_coords) > 1 @@ -263,12 +297,198 @@ async def render_ws(websocket: WebSocket, session_id: str) -> None: await websocket.close(code=1011, reason="internal error") -@app.get("/funcs/{session_id}") -async def get_funcs(session_id: str) -> dict[str, Any]: - if session_id not in _sessions: - raise HTTPException(status_code=404, detail="session not found") - trace = _sessions[session_id] - return {name: _serialize_func_stats(s) for name, s in trace.funcs.items()} +def _track_stores(session_id: str, start: int, end: int) -> list[dict[str, Any]]: + packets = _packets[session_id] + store_indices = _store_indices[session_id] + + end = min(end, len(packets)) + lo = np.searchsorted(store_indices, start, side="left") + hi = np.searchsorted(store_indices, end - 1, side="right") + + # func_name -> [xs, ys, func_stats] + pending: dict[str, list] = {} + + for store_i in range(lo, hi): + packet = packets[store_indices[store_i]] + func_stats = _get_func_item_for_packet(session_id, packet.func) + if func_stats is None: + continue + + func_name = func_stats["name"] + if func_name not in pending: + pending[func_name] = [[], [], func_stats] + xs_list, ys_list, _ = pending[func_name] + + min_coords = func_stats["min_coords"] + max_coords = func_stats["max_coords"] + min_x = min_coords[0] if min_coords else 0 + min_y = min_coords[1] if len(min_coords) > 1 else 0 + width = max_coords[0] - min_coords[0] + height = ( + max(1, max_coords[1] - min_coords[1]) + if len(min_coords) > 1 and len(max_coords) > 1 + else 1 + ) + + coords = np.asarray(packet.coordinates) + n_lanes = packet.type_lanes + dims_per_lane = len(coords) // n_lanes + + xs = coords[:n_lanes] - min_x + ys = ( + coords[n_lanes : 2 * n_lanes] - min_y + if dims_per_lane >= 2 + else np.full(n_lanes, -min_y, dtype=np.intp) + ) + mask = (xs >= 0) & (xs < width) & (ys >= 0) & (ys < height) + xs_list.extend(xs[mask].tolist()) + ys_list.extend(ys[mask].tolist()) + + return [ + {"func": func_name, "xs": xs_list, "ys": ys_list} + for func_name, (xs_list, ys_list, _) in pending.items() + if xs_list + ] + + +def _track_loads(session_id: str, start: int, end: int) -> list[dict[str, Any]]: + packets = _packets[session_id] + load_indices = _load_indices[session_id] + + end = min(end, len(packets)) + lo = np.searchsorted(load_indices, start, side="left") + hi = np.searchsorted(load_indices, end - 1, side="right") + + # func_name -> [xs, ys, func_stats] + pending: dict[str, list] = {} + + for load_i in range(lo, hi): + packet = packets[load_indices[load_i]] + func_stats = _get_func_item_for_packet(session_id, packet.func) + if func_stats is None: + continue + + func_name = func_stats["name"] + if func_name not in pending: + pending[func_name] = [[], [], func_stats] + xs_list, ys_list, _ = pending[func_name] + + min_coords = func_stats["min_coords"] + max_coords = func_stats["max_coords"] + min_x = min_coords[0] if min_coords else 0 + min_y = min_coords[1] if len(min_coords) > 1 else 0 + width = max_coords[0] - min_coords[0] + height = ( + max(1, max_coords[1] - min_coords[1]) + if len(min_coords) > 1 and len(max_coords) > 1 + else 1 + ) + + coords = np.asarray(packet.coordinates) + n_lanes = packet.type_lanes + dims_per_lane = len(coords) // n_lanes + + xs = coords[:n_lanes] - min_x + ys = ( + coords[n_lanes : 2 * n_lanes] - min_y + if dims_per_lane >= 2 + else np.full(n_lanes, -min_y, dtype=np.intp) + ) + mask = (xs >= 0) & (xs < width) & (ys >= 0) & (ys < height) + xs_list.extend(xs[mask].tolist()) + ys_list.extend(ys[mask].tolist()) + + return [ + {"func": func_name, "xs": xs_list, "ys": ys_list} + for func_name, (xs_list, ys_list, _) in pending.items() + if xs_list + ] + + +@app.websocket("/ws/{session_id}/loads") +async def render_loads_ws(websocket: WebSocket, session_id: str) -> None: + await websocket.accept() + + try: + if session_id not in _sessions: + await websocket.close(code=4004, reason="session not found") + return + + log.info("ws connected: session=%s", session_id) + + while True: + msg = await websocket.receive_json() + start: int = msg["start"] + end: int = msg["end"] + log.info("ws loads range request: start=%d end=%d", start, end) + + t0 = _time.perf_counter() + updates = _track_loads(session_id, start, end) + t1 = _time.perf_counter() + + await websocket.send_json( + {"updates": updates, "done": True, "start": start, "end": end} + ) + t2 = _time.perf_counter() + + log.info( + "track_loads render=%dms send=%dms total=%dms funcs=%d start=%d end=%d", + 1000 * (t1 - t0), + 1000 * (t2 - t1), + 1000 * (t2 - t0), + len(updates), + start, + end, + ) + + except WebSocketDisconnect: + pass + except Exception: + log.exception("WebSocket error for session %s", session_id) + await websocket.close(code=1011, reason="internal error") + + +@app.websocket("/ws/{session_id}/stores") +async def render_stores_ws(websocket: WebSocket, session_id: str) -> None: + await websocket.accept() + + try: + if session_id not in _sessions: + await websocket.close(code=4004, reason="session not found") + return + + log.info("ws connected: session=%s", session_id) + + while True: + msg = await websocket.receive_json() + start: int = msg["start"] + end: int = msg["end"] + log.info("ws stores range request: start=%d end=%d", start, end) + + t0 = _time.perf_counter() + updates = _track_stores(session_id, start, end) + t1 = _time.perf_counter() + + await websocket.send_json( + {"updates": updates, "done": True, "start": start, "end": end} + ) + t2 = _time.perf_counter() + + log.info( + "track_stores render=%dms send=%dms total=%dms funcs=%d start=%d end=%d", + 1000 * (t1 - t0), + 1000 * (t2 - t1), + 1000 * (t2 - t0), + len(updates), + start, + end, + ) + + except WebSocketDisconnect: + pass + except Exception: + log.exception("WebSocket error for session %s", session_id) + await websocket.close(code=1011, reason="internal error") @app.delete("/session/{session_id}") @@ -278,6 +498,7 @@ async def delete_session(session_id: str) -> dict[str, str]: del _sessions[session_id] del _packets[session_id] del _store_indices[session_id] + del _load_indices[session_id] del _func_name_cache[session_id] return {"deleted": session_id} diff --git a/apps/halidoscope/frontend/eslint.config.mjs b/apps/halidoscope/frontend/eslint.config.mjs index 65640e89e639..01614fdde846 100644 --- a/apps/halidoscope/frontend/eslint.config.mjs +++ b/apps/halidoscope/frontend/eslint.config.mjs @@ -14,5 +14,11 @@ export default defineConfig([ tseslint.configs.recommended, reactHooks.configs.flat.recommended, ], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { varsIgnorePattern: "^_", argsIgnorePattern: "^_" }, + ], + }, }, ]); diff --git a/apps/halidoscope/frontend/package.json b/apps/halidoscope/frontend/package.json index 8a9abdb9fb2f..32b6ff39931a 100644 --- a/apps/halidoscope/frontend/package.json +++ b/apps/halidoscope/frontend/package.json @@ -17,6 +17,7 @@ "@tauri-apps/plugin-opener": "^2", "@xyflow/react": "^12.11.0", "d3": "^7.9.0", + "jotai": "^2.20.1", "radix-ui": "^1.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -33,6 +34,6 @@ "eslint-plugin-react-hooks": "^7.1.1", "typescript": "~5.8.3", "typescript-eslint": "^8.60.1", - "vite": "^7.0.4" + "vite": "^8.0.16" } } diff --git a/apps/halidoscope/frontend/pnpm-lock.yaml b/apps/halidoscope/frontend/pnpm-lock.yaml index df525334b55d..b715b4b14f41 100644 --- a/apps/halidoscope/frontend/pnpm-lock.yaml +++ b/apps/halidoscope/frontend/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 3.0.0 '@tailwindcss/vite': specifier: ^4.3.0 - version: 4.3.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0)) + version: 4.3.0(vite@8.0.16(jiti@2.7.0)) '@tauri-apps/api': specifier: ^2 version: 2.11.0 @@ -29,6 +29,9 @@ importers: d3: specifier: ^7.9.0 version: 7.9.0 + jotai: + specifier: ^2.20.1 + version: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.15)(react@19.2.6) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -59,7 +62,7 @@ importers: version: 19.2.3(@types/react@19.2.15) '@vitejs/plugin-react': specifier: ^4.6.0 - version: 4.7.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0)) + version: 4.7.0(vite@8.0.16(jiti@2.7.0)) eslint: specifier: ^10.4.1 version: 10.4.1(jiti@2.7.0) @@ -73,8 +76,8 @@ importers: specifier: ^8.60.1 version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) vite: - specifier: ^7.0.4 - version: 7.3.5(jiti@2.7.0)(lightningcss@1.32.0) + specifier: ^8.0.16 + version: 8.0.16(jiti@2.7.0) packages: @@ -167,161 +170,14 @@ packages: '@dagrejs/graphlib@4.0.1': resolution: {integrity: sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==} - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} @@ -413,6 +269,15 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -1103,146 +968,106 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@rolldown/pluginutils@1.0.0-beta.27': - resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - - '@rollup/rollup-android-arm-eabi@4.61.0': - resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.61.0': - resolution: {integrity: sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==} + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.61.0': - resolution: {integrity: sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==} + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.61.0': - resolution: {integrity: sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==} + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.61.0': - resolution: {integrity: sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.61.0': - resolution: {integrity: sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==} + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.61.0': - resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.61.0': - resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.61.0': - resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.61.0': - resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.61.0': - resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.61.0': - resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.61.0': - resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.61.0': - resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.61.0': - resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} - cpu: [riscv64] - os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.61.0': - resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.61.0': - resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.61.0': - resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.61.0': - resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.61.0': - resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.61.0': - resolution: {integrity: sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==} + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.61.0': - resolution: {integrity: sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==} - cpu: [arm64] - os: [win32] + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] - '@rollup/rollup-win32-ia32-msvc@4.61.0': - resolution: {integrity: sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==} - cpu: [ia32] + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.61.0': - resolution: {integrity: sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==} + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.61.0': - resolution: {integrity: sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==} - cpu: [x64] - os: [win32] + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@tailwindcss/node@4.3.0': resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} @@ -1423,6 +1248,9 @@ packages: '@tauri-apps/plugin-opener@2.5.4': resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1840,11 +1668,6 @@ packages: resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1995,6 +1818,24 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jotai@2.20.1: + resolution: {integrity: sha512-dnuKfU/GLi8B28RRMjQ3AfoN7kfzP8o41+AX2FmITZqEMY8PHnjABq+VkEooomLwYaGjda+pgy0yFSjaHX/ZPg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@babel/core': '>=7.0.0' + '@babel/template': '>=7.0.0' + '@types/react': '>=17.0.0' + react: '>=17.0.0' + peerDependenciesMeta: + '@babel/core': + optional: true + '@babel/template': + optional: true + '@types/react': + optional: true + react: + optional: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2225,9 +2066,9 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rollup@4.61.0: - resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true rw@1.3.3: @@ -2330,15 +2171,16 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - vite@7.3.5: - resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -2349,12 +2191,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -2530,82 +2374,20 @@ snapshots: '@dagrejs/graphlib@4.0.1': {} - '@esbuild/aix-ppc64@0.27.7': - optional: true - - '@esbuild/android-arm64@0.27.7': - optional: true - - '@esbuild/android-arm@0.27.7': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - - '@esbuild/linux-x64@0.27.7': - optional: true - - '@esbuild/netbsd-arm64@0.27.7': - optional: true - - '@esbuild/netbsd-x64@0.27.7': - optional: true - - '@esbuild/openbsd-arm64@0.27.7': - optional: true - - '@esbuild/openbsd-x64@0.27.7': - optional: true - - '@esbuild/openharmony-arm64@0.27.7': - optional: true - - '@esbuild/sunos-x64@0.27.7': - optional: true - - '@esbuild/win32-arm64@0.27.7': + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 optional: true - '@esbuild/win32-ia32@0.27.7': + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/win32-x64@0.27.7': + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 optional: true '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.7.0))': @@ -2694,6 +2476,15 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.133.0': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -3441,82 +3232,58 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@rolldown/pluginutils@1.0.0-beta.27': {} - - '@rollup/rollup-android-arm-eabi@4.61.0': - optional: true - - '@rollup/rollup-android-arm64@4.61.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.61.0': + '@rolldown/binding-android-arm64@1.0.3': optional: true - '@rollup/rollup-darwin-x64@4.61.0': + '@rolldown/binding-darwin-arm64@1.0.3': optional: true - '@rollup/rollup-freebsd-arm64@4.61.0': + '@rolldown/binding-darwin-x64@1.0.3': optional: true - '@rollup/rollup-freebsd-x64@4.61.0': + '@rolldown/binding-freebsd-x64@1.0.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.61.0': + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.61.0': + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.61.0': + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rollup/rollup-linux-loong64-gnu@4.61.0': + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - '@rollup/rollup-linux-loong64-musl@4.61.0': + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.61.0': + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - '@rollup/rollup-linux-ppc64-musl@4.61.0': + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.61.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.61.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.61.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.61.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.61.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.61.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.61.0': + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rollup/rollup-win32-arm64-msvc@4.61.0': + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - '@rollup/rollup-win32-ia32-msvc@4.61.0': + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true - '@rollup/rollup-win32-x64-gnu@4.61.0': - optional: true + '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/rollup-win32-x64-msvc@4.61.0': - optional: true + '@rolldown/pluginutils@1.0.1': {} '@tailwindcss/node@4.3.0': dependencies: @@ -3579,12 +3346,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - '@tailwindcss/vite@4.3.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0))': + '@tailwindcss/vite@4.3.0(vite@8.0.16(jiti@2.7.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 7.3.5(jiti@2.7.0)(lightningcss@1.32.0) + vite: 8.0.16(jiti@2.7.0) '@tauri-apps/api@2.11.0': {} @@ -3643,6 +3410,11 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.0 + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -3888,7 +3660,7 @@ snapshots: '@typescript-eslint/types': 8.60.1 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@4.7.0(vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0))': + '@vitejs/plugin-react@4.7.0(vite@8.0.16(jiti@2.7.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -3896,7 +3668,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.3.5(jiti@2.7.0)(lightningcss@1.32.0) + vite: 8.0.16(jiti@2.7.0) transitivePeerDependencies: - supports-color @@ -4147,35 +3919,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -4324,6 +4067,13 @@ snapshots: jiti@2.7.0: {} + jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.15)(react@19.2.6): + optionalDependencies: + '@babel/core': 7.29.7 + '@babel/template': 7.29.7 + '@types/react': 19.2.15 + react: 19.2.6 + js-tokens@4.0.0: {} jsesc@3.1.0: {} @@ -4554,36 +4304,26 @@ snapshots: robust-predicates@3.0.3: {} - rollup@4.61.0: + rolldown@1.0.3: dependencies: - '@types/estree': 1.0.9 + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.61.0 - '@rollup/rollup-android-arm64': 4.61.0 - '@rollup/rollup-darwin-arm64': 4.61.0 - '@rollup/rollup-darwin-x64': 4.61.0 - '@rollup/rollup-freebsd-arm64': 4.61.0 - '@rollup/rollup-freebsd-x64': 4.61.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.61.0 - '@rollup/rollup-linux-arm-musleabihf': 4.61.0 - '@rollup/rollup-linux-arm64-gnu': 4.61.0 - '@rollup/rollup-linux-arm64-musl': 4.61.0 - '@rollup/rollup-linux-loong64-gnu': 4.61.0 - '@rollup/rollup-linux-loong64-musl': 4.61.0 - '@rollup/rollup-linux-ppc64-gnu': 4.61.0 - '@rollup/rollup-linux-ppc64-musl': 4.61.0 - '@rollup/rollup-linux-riscv64-gnu': 4.61.0 - '@rollup/rollup-linux-riscv64-musl': 4.61.0 - '@rollup/rollup-linux-s390x-gnu': 4.61.0 - '@rollup/rollup-linux-x64-gnu': 4.61.0 - '@rollup/rollup-linux-x64-musl': 4.61.0 - '@rollup/rollup-openbsd-x64': 4.61.0 - '@rollup/rollup-openharmony-arm64': 4.61.0 - '@rollup/rollup-win32-arm64-msvc': 4.61.0 - '@rollup/rollup-win32-ia32-msvc': 4.61.0 - '@rollup/rollup-win32-x64-gnu': 4.61.0 - '@rollup/rollup-win32-x64-msvc': 4.61.0 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 rw@1.3.3: {} @@ -4664,18 +4404,16 @@ snapshots: dependencies: react: 19.2.6 - vite@7.3.5(jiti@2.7.0)(lightningcss@1.32.0): + vite@8.0.16(jiti@2.7.0): dependencies: - esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) + lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.15 - rollup: 4.61.0 + rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: fsevents: 2.3.3 jiti: 2.7.0 - lightningcss: 1.32.0 which@2.0.2: dependencies: diff --git a/apps/halidoscope/frontend/src/App.css b/apps/halidoscope/frontend/src/App.css index 101266e72787..781722bcf57d 100644 --- a/apps/halidoscope/frontend/src/App.css +++ b/apps/halidoscope/frontend/src/App.css @@ -22,14 +22,45 @@ @theme { --color-ps-primary: oklch(0.4423 0 0); --color-ps-secondary: oklch(0.2768 0 0); - --color-ps-text: oklch(0.8975 0 0); + --color-ps-titlebar: oklch(0.3791 0 0); + --color-ps-text-primary: oklch(0.8975 0 0); + --color-ps-text-secondary: oklch(0.7444 0 0); --color-ps-border-primary: oklch(0.3407 0 0); --color-ps-border-secondary: oklch(0.3979 0 0); --color-ps-border-tertiary: oklch(0.4997 0 0); + + --text-tiny: 0.625rem; + --text-tiny--line-height: 1.5; } @layer components { .text-responsive { font-size: clamp(0.5rem, calc(1rem / var(--zoom-level)), 1.5rem); } + + @keyframes slideDown { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes slideUp { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } + + .accordion-content[data-state="open"] { + animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1); + } + + .accordion-content[data-state="closed"] { + animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1); + } } diff --git a/apps/halidoscope/frontend/src/App.tsx b/apps/halidoscope/frontend/src/App.tsx index 52abfa47d4af..e407063f1e67 100644 --- a/apps/halidoscope/frontend/src/App.tsx +++ b/apps/halidoscope/frontend/src/App.tsx @@ -1,17 +1,12 @@ import { invoke } from "@tauri-apps/api/core"; import { getMatches } from "@tauri-apps/plugin-cli"; -import { ReactFlowProvider } from "@xyflow/react"; import * as React from "react"; -import Canvas from "./components/Canvas"; -import Sidebar from "./components/Sidebar"; -import Timeline from "./components/Timeline"; -import { - CanvasRegistry, - CanvasRegistryProvider, -} from "./hooks/canvas-registry"; -import { FuncStats } from "./types"; -import { loadTracePath, deregisterTrace } from "./utils/api"; +import ViewTabs from "@/components/views/ViewTabs"; +import type { FuncStats } from "@/types"; +import { CanvasRegistry } from "@/hooks/canvas-registry"; +import { TraceContextProvider } from "@/hooks/trace"; +import { loadTracePath, deregisterTrace } from "@/utils/api"; import "./App.css"; @@ -22,6 +17,9 @@ function App() { const [packetCount, setPacketCount] = React.useState(0); const [canvasRegistry, setCanvasRegistry] = React.useState(null); + const [globalMaxStoreCount, setGlobalMaxStoreCount] = + React.useState(0); + const [globalMaxLoadCount, setGlobalMaxLoadCount] = React.useState(0); React.useEffect(() => { const controller = new AbortController(); @@ -39,15 +37,22 @@ function App() { : `${await invoke("get_cwd")}/${tracePath}`; try { - const { session_id, funcs, dag_edges, num_packets } = - await loadTracePath(resolved, controller.signal); + const { + session_id, + funcs, + dag_edges, + num_packets, + global_max_store_count, + global_max_load_count, + } = await loadTracePath(resolved, controller.signal); loadedSessionId = session_id; setSessionId(session_id); setFuncs(funcs); setDagEdges(dag_edges); setPacketCount(num_packets); - + setGlobalMaxStoreCount(global_max_store_count); + setGlobalMaxLoadCount(global_max_load_count); setCanvasRegistry(new CanvasRegistry()); } catch (err) { if ((err as Error).name !== "AbortError") { @@ -69,29 +74,21 @@ function App() { }, []); return ( - -
- -
-
- {Object.keys(funcs).length > 0 ? ( - - - - ) : ( -
-

Loading trace...

-
- )} -
- -
+ +
+
- +
); } diff --git a/apps/halidoscope/frontend/src/components/Canvas.tsx b/apps/halidoscope/frontend/src/components/Canvas.tsx deleted file mode 100644 index ede6145c38f6..000000000000 --- a/apps/halidoscope/frontend/src/components/Canvas.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react"; -import { ReactFlow, useViewport } from "@xyflow/react"; - -import FuncCanvas from "./FuncCanvas"; -import { FuncStats } from "../types"; -import { buildEdges, buildNodes, getLayoutedElements } from "../utils/graph"; - -interface CanvasProps { - funcs: Record; - dagEdges: Record; -} - -const NODE_TYPES = { - funcCanvas: FuncCanvas, -}; - -function Canvas({ funcs, dagEdges }: CanvasProps) { - const { nodes, edges } = React.useMemo(() => { - return getLayoutedElements(buildNodes(funcs), buildEdges(dagEdges)); - }, [funcs, dagEdges]); - - const { zoom } = useViewport(); - - React.useEffect(() => { - document.documentElement.style.setProperty("--zoom-level", zoom.toString()); - }, [zoom]); - - return ( -
- -
- Zoom: {Math.round(zoom * 100)}% -
-
- ); -} - -export default Canvas; diff --git a/apps/halidoscope/frontend/src/components/FuncCanvas.tsx b/apps/halidoscope/frontend/src/components/FuncCanvas.tsx deleted file mode 100644 index 88dd44a641de..000000000000 --- a/apps/halidoscope/frontend/src/components/FuncCanvas.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react"; -import type { Node, NodeProps } from "@xyflow/react"; - -import { useCanvasRegistry } from "../hooks/canvas-registry"; -import { NodeData, ChannelData } from "../types"; - -type FuncNode = Node; - -function FuncCanvas({ - id, - data: { name, width, height }, -}: NodeProps) { - const canvasRef = React.useRef(null); - const canvasRegistry = useCanvasRegistry(); - - const applyChannel = React.useCallback( - (imageData: ImageDataArray, ch: ChannelData, offset: number) => { - for (let i = 0; i < ch.xs.length; i++) { - imageData[4 * (ch.ys[i] * width + ch.xs[i]) + offset] = ch.values[i]; - } - }, - [width], - ); - - React.useEffect(() => { - const ctx = canvasRef.current?.getContext("2d"); - if (!ctx) return; - - const image = ctx.createImageData(width, height); - - const reset = () => { - const data = image.data; - data.fill(0); - // Opaque black background. - for (let i = 3; i < data.length; i += 4) data[i] = 255; - ctx.putImageData(image, 0, 0); - }; - - reset(); - - const unregister = canvasRegistry.register(id, { - draw: ({ xs, ys, values, r, g, b }) => { - const data = image.data; - - if (r) { - applyChannel(data, r, 0); - } - - if (g) { - applyChannel(data, g, 1); - } - - if (b) { - applyChannel(data, b, 2); - } - - if (xs && ys && values) { - for (let i = 0; i < xs.length; i++) { - const idx = 4 * (ys[i] * width + xs[i]); - const v = values[i]; - data[idx] = v; - data[idx + 1] = v; - data[idx + 2] = v; - data[idx + 3] = 255; - } - } - - ctx.putImageData(image, 0, 0); - }, - clear: reset, - }); - - return unregister; - }, [id, width, height, canvasRegistry, applyChannel]); - - return ( -
- - {name} - - -
- ); -} - -export default FuncCanvas; diff --git a/apps/halidoscope/frontend/src/components/Sidebar.tsx b/apps/halidoscope/frontend/src/components/Sidebar.tsx deleted file mode 100644 index 58515d039a3c..000000000000 --- a/apps/halidoscope/frontend/src/components/Sidebar.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { FuncStats } from "../types"; - -interface SidebarProps { - funcs: Record; -} - -function Sidebar({ funcs }: SidebarProps) { - return ( -
- -
-
-
- ); -} - -export default Sidebar; diff --git a/apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx b/apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx new file mode 100644 index 000000000000..5465372709a0 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx @@ -0,0 +1,39 @@ +import { Checkbox } from "radix-ui"; + +interface ControlPanelProps { + setHidden: React.Dispatch>; +} + +function ControlPanel({ setHidden }: ControlPanelProps) { + return ( +
+
+ setHidden(checked === false)} + > + + + + + + + +
+
+ ); +} + +export default ControlPanel; diff --git a/apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx b/apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx new file mode 100644 index 000000000000..062425b4ec24 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx @@ -0,0 +1,41 @@ +import { Tabs } from "radix-ui"; + +import FuncsPanel from "@/components/controls/funcs/FuncsPanel"; +import PlaybackPanel from "@/components/controls/playback/PlaybackPanel"; +import { FuncStats } from "@/types"; + +function ControlTabs({ funcs }: { funcs: Record }) { + return ( +
+
+
+ + + + Funcs + + + Playback + + + + + + + + + +
+ ); +} + +export default ControlTabs; diff --git a/apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx b/apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx new file mode 100644 index 000000000000..824d05b4dc47 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx @@ -0,0 +1,86 @@ +import { useAtom } from "jotai"; +import { Accordion } from "radix-ui"; + +import type { FuncStats } from "@/types"; +import { funcAtom } from "@/state/func"; + +interface FuncsPanelProps { + funcs: Record; +} + +function FuncsPanel({ funcs }: FuncsPanelProps) { + const [func, setFunc] = useAtom(funcAtom); + + return ( + setFunc(value)} + > + {Object.values(funcs).map((func) => ( + + + + + + {func.name} + + +
+ + Minimum Coordinates + + + ({func.min_coords.join(",")}) + + + Maximum Coordinates + + + ({func.max_coords.join(",")}) + + + Minimum Value + + + {func.min_value} + + + Maximum Value + + + {func.max_value} + + + Maximum Store Count + + + {func.max_store_count} + + + Maximum Load Count + + + {func.max_load_count} + +
+
+
+ ))} +
+ ); +} + +export default FuncsPanel; diff --git a/apps/halidoscope/frontend/src/components/controls/playback/PlaybackLegend.tsx b/apps/halidoscope/frontend/src/components/controls/playback/PlaybackLegend.tsx new file mode 100644 index 000000000000..46ac7ab2f65c --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/playback/PlaybackLegend.tsx @@ -0,0 +1,81 @@ +import * as d3 from "d3"; +import * as React from "react"; + +import { useTraceContext } from "@/hooks/trace"; +import type { PlaybackMode } from "@/state/playback"; + +const RAMP_HEIGHT = 16; + +function materializeColorRamp( + interpolator: (t: number) => string, + direction: "Forward" | "Reverse", + n: number, +): string[] { + const colors: string[] = []; + + for (let i = 0; i <= n; i++) { + colors.push( + d3 + .rgb(interpolator((direction === "Forward" ? i : n - i) / n)) + .formatHex(), + ); + } + + return colors; +} + +interface PlaybackLegendProps { + playbackMode: PlaybackMode; +} + +function PlaybackLegend({ playbackMode }: PlaybackLegendProps) { + const canvas = React.useRef(null); + const colors = materializeColorRamp( + playbackMode === "stores" ? d3.interpolateReds : d3.interpolateBlues, + "Forward", + 256, + ); + const { globalMaxStoreCount, globalMaxLoadCount } = useTraceContext(); + + const drawRamp = React.useCallback( + (ctx: CanvasRenderingContext2D) => { + ctx.clearRect(0, 0, canvas.current!.width, canvas.current!.height); + + for (let i = 0; i < colors.length; ++i) { + ctx.fillStyle = colors[i]; + ctx.fillRect(i, 0, 1, RAMP_HEIGHT); + } + }, + [colors], + ); + + React.useEffect(() => { + if (canvas.current) { + const ctx = canvas.current?.getContext("2d"); + + canvas.current.style.width = "100%"; + canvas.current.style.height = `${RAMP_HEIGHT}px`; + + if (ctx) { + drawRamp(ctx); + } + } + }, [drawRamp]); + + return ( +
+ + {playbackMode === "stores" ? "Store Count →" : "Load Count →"} + + +
+ 0 + + {playbackMode === "stores" ? globalMaxStoreCount : globalMaxLoadCount} + +
+
+ ); +} + +export default PlaybackLegend; diff --git a/apps/halidoscope/frontend/src/components/controls/playback/PlaybackPanel.tsx b/apps/halidoscope/frontend/src/components/controls/playback/PlaybackPanel.tsx new file mode 100644 index 000000000000..f403e3982506 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/playback/PlaybackPanel.tsx @@ -0,0 +1,53 @@ +import { useAtom } from "jotai"; +import { RadioGroup } from "radix-ui"; + +import PlaybackLegend from "@/components/controls/playback/PlaybackLegend"; +import { playbackModeAtom, type PlaybackMode } from "@/state/playback"; + +const PLAYBACK_MODES = [ + { value: "normal", label: "Normal" }, + { value: "stores", label: "Stores" }, + { value: "loads", label: "Loads" }, +] as const; + +function PlaybackPanel() { + const [playbackMode, setPlaybackMode] = useAtom(playbackModeAtom); + + return ( +
+
+ + setPlaybackMode(value as PlaybackMode)} + className="flex gap-3" + > + {PLAYBACK_MODES.map(({ value, label }) => ( +
+ + + + +
+ ))} +
+
+ {playbackMode !== "normal" ? ( + + ) : null} +
+ ); +} + +export default PlaybackPanel; diff --git a/apps/halidoscope/frontend/src/components/shared/Canvas.tsx b/apps/halidoscope/frontend/src/components/shared/Canvas.tsx new file mode 100644 index 000000000000..d940481a5962 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/shared/Canvas.tsx @@ -0,0 +1,84 @@ +import { + ReactFlow, + useEdgesState, + useNodesState, + useViewport, + type Node, + type Edge, +} from "@xyflow/react"; +import { useAtom } from "jotai"; +import * as React from "react"; + +import FuncCanvas from "@/components/views/tracer/FuncCanvas"; +import FuncEdge from "@/components/views/tracer/FuncEdge"; +import { FuncStats, NodeTypes } from "@/types"; +import { buildEdges, buildNodes, getLayoutedElements } from "@/utils/graph"; +import { funcAtom } from "@/state/func"; + +const NODE_TYPES = { + funcCanvas: FuncCanvas, +}; + +const EDGE_TYPES = { + funcEdge: FuncEdge, +}; + +function hideEdge(hidden: boolean) { + return function handleVisibilityChange(edge: Edge) { + return { + ...edge, + hidden, + }; + }; +} + +interface CanvasProps { + funcs: Record; + dagEdges: Record; + type: NodeTypes; +} + +function Canvas({ funcs, dagEdges, type }: CanvasProps) { + const { nodes: initialNodes, edges: initialEdges } = React.useMemo(() => { + return getLayoutedElements(buildNodes(funcs, type), buildEdges(dagEdges)); + }, [funcs, dagEdges, type]); + + const [nodes, _setNodes, onNodesChange] = + useNodesState>(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + const [hidden, _setHidden] = React.useState(false); + const [_func, setFunc] = useAtom(funcAtom); + + const { zoom } = useViewport(); + + React.useEffect(() => { + document.documentElement.style.setProperty("--zoom-level", zoom.toString()); + }, [zoom]); + + React.useEffect(() => { + setEdges((eds) => eds.map(hideEdge(hidden))); + }, [hidden, setEdges]); + + return ( +
+ setFunc(node.data.name)} + /> +
+ Zoom: {Math.round(zoom * 100)}% +
+
+ ); +} + +export default Canvas; diff --git a/apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx b/apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx new file mode 100644 index 000000000000..8de719581946 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx @@ -0,0 +1,17 @@ +function HandleCircle({ zoom }: { zoom: number }) { + return ( + + + + ); +} + +export default HandleCircle; diff --git a/apps/halidoscope/frontend/src/components/views/ViewTabs.tsx b/apps/halidoscope/frontend/src/components/views/ViewTabs.tsx new file mode 100644 index 000000000000..186692ab9524 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/views/ViewTabs.tsx @@ -0,0 +1,26 @@ +import { Tabs } from "radix-ui"; + +import Tracer from "@/components/views/tracer/Tracer"; + +function ViewTabs() { + return ( + + + + Tracer + + + + + + + ); +} + +export default ViewTabs; diff --git a/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx b/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx new file mode 100644 index 000000000000..91cd6e942c3f --- /dev/null +++ b/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx @@ -0,0 +1,248 @@ +import { + Handle, + Position, + type Node, + type NodeProps, + useViewport, +} from "@xyflow/react"; +import * as d3 from "d3"; +import { useAtom } from "jotai"; +import * as React from "react"; + +import HandleCircle from "@/components/shared/HandleCircle"; +import { useCanvasRegistry } from "@/hooks/canvas-registry"; +import type { FuncStats, ChannelData } from "@/types"; +import { useTraceContext } from "@/hooks/trace"; +import { playbackModeAtom } from "@/state/playback"; + +type FuncNode = Node; + +function FuncCanvas({ + id, + data: { name, width, height }, +}: NodeProps) { + const canvasRef = React.useRef(null); + const canvasRegistry = useCanvasRegistry(); + const [playbackMode] = useAtom(playbackModeAtom); + const { globalMaxStoreCount, globalMaxLoadCount } = useTraceContext(); + const { zoom } = useViewport(); + + const storeScale = React.useMemo( + () => + d3.scaleSequential(d3.interpolateReds).domain([0, globalMaxStoreCount]), + [globalMaxStoreCount], + ); + + const loadScale = React.useMemo( + () => + d3.scaleSequential(d3.interpolateBlues).domain([0, globalMaxLoadCount]), + [globalMaxLoadCount], + ); + + const applyChannel = React.useCallback( + (imageData: ImageDataArray, ch: ChannelData, offset: number) => { + for (let i = 0; i < ch.xs.length; i++) { + imageData[4 * (ch.ys[i] * width + ch.xs[i]) + offset] = ch.values[i]; + } + }, + [width], + ); + + const drawNormal = React.useCallback( + ({ + xs, + ys, + values, + r, + g, + b, + image, + ctx, + }: { + xs?: number[]; + ys?: number[]; + values?: number[]; + r?: ChannelData; + g?: ChannelData; + b?: ChannelData; + image: ImageData; + ctx: CanvasRenderingContext2D; + }) => { + const data = image.data; + + if (r) { + applyChannel(data, r, 0); + } + + if (g) { + applyChannel(data, g, 1); + } + + if (b) { + applyChannel(data, b, 2); + } + + if (xs && ys && values) { + for (let i = 0; i < xs.length; i++) { + const idx = 4 * (ys[i] * width + xs[i]); + const v = values[i]; + data[idx] = v; + data[idx + 1] = v; + data[idx + 2] = v; + data[idx + 3] = 255; + } + } + + ctx.putImageData(image, 0, 0); + }, + [applyChannel, width], + ); + + const drawStoreCounts = React.useCallback( + ({ + xs, + ys, + counts, + storeCountBuf, + image, + ctx, + }: { + xs?: number[]; + ys?: number[]; + counts?: number[]; + storeCountBuf: Int32Array; + image: ImageData; + ctx: CanvasRenderingContext2D; + }) => { + if (!xs || !ys) return; + + const data = image.data; + + for (let i = 0; i < xs.length; i++) { + const idx = ys[i] * width + xs[i]; + storeCountBuf[idx] += counts ? counts[i] : 1; + const { r, g, b } = d3.color(storeScale(storeCountBuf[idx]))!.rgb(); + data[4 * idx] = r; + data[4 * idx + 1] = g; + data[4 * idx + 2] = b; + data[4 * idx + 3] = 255; + } + + ctx.putImageData(image, 0, 0); + }, + [storeScale, width], + ); + + const drawLoadCounts = React.useCallback( + ({ + xs, + ys, + counts, + loadCountBuf, + image, + ctx, + }: { + xs?: number[]; + ys?: number[]; + counts?: number[]; + loadCountBuf: Int32Array; + image: ImageData; + ctx: CanvasRenderingContext2D; + }) => { + if (!xs || !ys) return; + + const data = image.data; + + for (let i = 0; i < xs.length; i++) { + const idx = ys[i] * width + xs[i]; + loadCountBuf[idx] += counts ? counts[i] : 1; + const { r, g, b } = d3.color(loadScale(loadCountBuf[idx]))!.rgb(); + data[4 * idx] = r; + data[4 * idx + 1] = g; + data[4 * idx + 2] = b; + data[4 * idx + 3] = 255; + } + + ctx.putImageData(image, 0, 0); + }, + [loadScale, width], + ); + + React.useEffect(() => { + const ctx = canvasRef.current?.getContext("2d"); + if (!ctx) return; + + const image = ctx.createImageData(width, height); + const storeCountBuf = new Int32Array(width * height); + const loadCountBuf = new Int32Array(width * height); + + const reset = () => { + const data = image.data; + + data.fill(0); + // Opaque black background. + for (let i = 3; i < data.length; i += 4) data[i] = 255; + ctx.putImageData(image, 0, 0); + + // Reset the store/load count buffers. + storeCountBuf.fill(0); + loadCountBuf.fill(0); + }; + + reset(); + + const unregister = canvasRegistry.register(id, { + draw: ({ xs, ys, values, counts, r, g, b }) => { + switch (playbackMode) { + case "normal": + drawNormal({ xs, ys, values, r, g, b, image, ctx }); + break; + case "stores": + drawStoreCounts({ xs, ys, counts, storeCountBuf, image, ctx }); + break; + case "loads": + drawLoadCounts({ xs, ys, counts, loadCountBuf, image, ctx }); + break; + } + }, + clear: reset, + }); + + return unregister; + }, [ + id, + width, + height, + canvasRegistry, + applyChannel, + drawNormal, + drawStoreCounts, + drawLoadCounts, + playbackMode, + ]); + + return ( +
+ + {name} + + + + + + + + +
+ ); +} + +export default FuncCanvas; diff --git a/apps/halidoscope/frontend/src/components/views/tracer/FuncEdge.tsx b/apps/halidoscope/frontend/src/components/views/tracer/FuncEdge.tsx new file mode 100644 index 000000000000..bada3120c706 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/views/tracer/FuncEdge.tsx @@ -0,0 +1,18 @@ +import { BaseEdge, getStraightPath, type EdgeProps } from "@xyflow/react"; + +function FuncEdge({ id, sourceX, sourceY, targetX, targetY }: EdgeProps) { + const [edgePath] = getStraightPath({ + sourceX, + sourceY, + targetX, + targetY, + }); + + return ( + <> + + + ); +} + +export default FuncEdge; diff --git a/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx b/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx new file mode 100644 index 000000000000..07fc33ac3bcd --- /dev/null +++ b/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx @@ -0,0 +1,42 @@ +import { ReactFlowProvider } from "@xyflow/react"; + +import Canvas from "@/components/shared/Canvas"; +import Timeline from "@/components/views/tracer/TracerTimeline"; +import { CanvasRegistryProvider } from "@/hooks/canvas-registry"; +import { useTraceContext } from "@/hooks/trace"; +import ControlTabs from "@/components/controls/ControlTabs"; + +function Tracer() { + const { sessionId, funcs, dagEdges, packetCount, canvasRegistry } = + useTraceContext(); + + return ( + +
+
+
+ {Object.keys(funcs).length > 0 ? ( + <> + + + + + + ) : ( +
+

Loading trace...

+
+ )} +
+ +
+
+
+ ); +} + +export default Tracer; diff --git a/apps/halidoscope/frontend/src/components/Timeline.tsx b/apps/halidoscope/frontend/src/components/views/tracer/TracerTimeline.tsx similarity index 85% rename from apps/halidoscope/frontend/src/components/Timeline.tsx rename to apps/halidoscope/frontend/src/components/views/tracer/TracerTimeline.tsx index e2bd79a61dfd..268bb51d6b24 100644 --- a/apps/halidoscope/frontend/src/components/Timeline.tsx +++ b/apps/halidoscope/frontend/src/components/views/tracer/TracerTimeline.tsx @@ -1,26 +1,33 @@ import * as d3 from "d3"; +import { useAtom } from "jotai"; import { Slider } from "radix-ui"; import * as React from "react"; -import { CanvasRegistry } from "../hooks/canvas-registry"; -import { RangeRequest, RenderResponse } from "../types"; +import { CanvasRegistry } from "@/hooks/canvas-registry"; +import { playbackModeAtom } from "@/state/playback"; +import { RangeRequest, RenderResponse } from "@/types"; import { PLAYBACK_INTERVAL_MS, PLAYBACK_STEP, SCRUB_DEBOUNCE_MS, WS_ENDPOINT, -} from "../utils/constants"; +} from "@/utils/constants"; -interface TimelineProps { +interface TracerTimelineProps { packetCount: number; sessionId: string; canvasRegistry: CanvasRegistry | null; } -function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { +function TracerTimeline({ + packetCount, + sessionId, + canvasRegistry, +}: TracerTimelineProps) { // Track the current packet index. const [packetIndex, setPacketIndex] = React.useState(0); const [playing, setPlaying] = React.useState(false); + const [playbackMode] = useAtom(playbackModeAtom); const wsRef = React.useRef(null); // Use refs to synchronously track mutable state without triggering re-renders. @@ -70,7 +77,11 @@ function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { // Send the request over the WebSocket. inFlightRef.current = true; pendingEndRef.current = targetEnd; - const request: RangeRequest = { start, end: targetEnd }; + const request: RangeRequest = { + start, + end: targetEnd, + }; + wsRef.current.send(JSON.stringify(request)); }, [canvasRegistry]); @@ -108,7 +119,19 @@ function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { return; } - wsRef.current = new WebSocket(`${WS_ENDPOINT}/ws/${sessionId}`); + let wsPath; + switch (playbackMode) { + case "loads": + wsPath = `${WS_ENDPOINT}/ws/${sessionId}/loads`; + break; + case "stores": + wsPath = `${WS_ENDPOINT}/ws/${sessionId}/stores`; + break; + default: + wsPath = `${WS_ENDPOINT}/ws/${sessionId}`; + break; + } + wsRef.current = new WebSocket(wsPath); wsRef.current.onopen = () => { pump(); @@ -146,7 +169,7 @@ function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { wsRef.current = null; } }; - }, [pump, canvasRegistry, sessionId]); + }, [pump, canvasRegistry, sessionId, playbackMode]); // Playback loop: advance the playhead on a fixed interval and pump after each // step. Rendering may lag the playhead on large traces; it catches up via the @@ -211,12 +234,12 @@ function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { {ticks.map((tick) => (
-

+

{d3.format(".2s")(tick)}

@@ -230,19 +253,19 @@ function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { value={[packetIndex]} disabled={disabled} > - +
- Packets - + Packets + {packetIndex.toLocaleString()} /{" "} {Math.max(packetCount - 1, 0).toLocaleString()} @@ -251,4 +274,4 @@ function Timeline({ packetCount, sessionId, canvasRegistry }: TimelineProps) { ); } -export default Timeline; +export default TracerTimeline; diff --git a/apps/halidoscope/frontend/src/hooks/trace.ts b/apps/halidoscope/frontend/src/hooks/trace.ts new file mode 100644 index 000000000000..9173194c616c --- /dev/null +++ b/apps/halidoscope/frontend/src/hooks/trace.ts @@ -0,0 +1,25 @@ +import * as React from "react"; + +import { FuncStats } from "@/types"; +import { CanvasRegistry } from "@/hooks/canvas-registry"; + +const TraceContext = React.createContext<{ + sessionId: string; + funcs: Record; + dagEdges: Record; + packetCount: number; + canvasRegistry: CanvasRegistry | null; + globalMaxStoreCount: number; + globalMaxLoadCount: number; +}>({ + sessionId: "", + funcs: {}, + dagEdges: {}, + packetCount: 0, + canvasRegistry: null, + globalMaxStoreCount: 0, + globalMaxLoadCount: 0, +}); + +export const TraceContextProvider = TraceContext.Provider; +export const useTraceContext = () => React.useContext(TraceContext); diff --git a/apps/halidoscope/frontend/src/state/func.ts b/apps/halidoscope/frontend/src/state/func.ts new file mode 100644 index 000000000000..87a03d02956b --- /dev/null +++ b/apps/halidoscope/frontend/src/state/func.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const funcAtom = atom(null); diff --git a/apps/halidoscope/frontend/src/state/playback.ts b/apps/halidoscope/frontend/src/state/playback.ts new file mode 100644 index 000000000000..5eeeaf6735a5 --- /dev/null +++ b/apps/halidoscope/frontend/src/state/playback.ts @@ -0,0 +1,5 @@ +import { atom } from "jotai"; + +export type PlaybackMode = "normal" | "stores" | "loads"; + +export const playbackModeAtom = atom("normal"); diff --git a/apps/halidoscope/frontend/src/types/index.ts b/apps/halidoscope/frontend/src/types/index.ts index 46270724469a..abe1d879f600 100644 --- a/apps/halidoscope/frontend/src/types/index.ts +++ b/apps/halidoscope/frontend/src/types/index.ts @@ -1,14 +1,13 @@ -export interface FuncStats { +export interface FuncStats extends Record { name: string; + width: number; + height: number; min_coords: number[]; max_coords: number[]; min_value: number; max_value: number; -} - -export interface NodeData extends Record, FuncStats { - width: number; - height: number; + max_store_count: number; + max_load_count: number; } /** Per-channel pixel writes for one color channel of a color func. */ @@ -26,6 +25,7 @@ export interface ChannelData { * @property xs The x-coordinates of the updated pixels. * @property ys The y-coordinates of the updated pixels. * @property values Normalized 0-255 values for the updated pixels (Grayscale). + * @property counts Incremental store counts at each (x, y) for the stores endpoint. * @property r The red channel for the updated pixels. * @property g The green channel for the updated pixels. * @property b The blue channel for the updated pixels. @@ -35,12 +35,15 @@ export interface FuncUpdate { xs?: number[]; ys?: number[]; values?: number[]; + counts?: number[]; r?: ChannelData; g?: ChannelData; b?: ChannelData; } -/** A range request sent to the backend WebSocket. Renders stores in [start, end). */ +/** + * A range request sent to the backend WebSocket. Renders packets in [start, end). + */ export interface RangeRequest { start: number; end: number; @@ -53,3 +56,5 @@ export interface RenderResponse { start: number; end: number; } + +export type NodeTypes = "funcCanvas"; diff --git a/apps/halidoscope/frontend/src/utils/api.ts b/apps/halidoscope/frontend/src/utils/api.ts index 86d629fe6a9b..23f0680db90c 100644 --- a/apps/halidoscope/frontend/src/utils/api.ts +++ b/apps/halidoscope/frontend/src/utils/api.ts @@ -7,6 +7,8 @@ interface LoadTraceResponse { funcs: Record; dag_edges: Record; num_packets: number; + global_max_store_count: number; + global_max_load_count: number; } /** diff --git a/apps/halidoscope/frontend/src/utils/func.ts b/apps/halidoscope/frontend/src/utils/func.ts deleted file mode 100644 index 31cc0d9ac155..000000000000 --- a/apps/halidoscope/frontend/src/utils/func.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { FuncStats } from "../types"; - -/** - * Compute the width and height of a function's bounding box based on its min - * and max coordinates. - * - * @param stats The {@link FuncStats} of the Halide func. - * @returns The computed width and height of the func's bounding box. - */ -export function computeFuncSize(stats: FuncStats): { - width: number; - height: number; -} { - const width = (stats.max_coords[0] ?? 0) - (stats.min_coords[0] ?? 0) || 1; - const height = (stats.max_coords[1] ?? 0) - (stats.min_coords[1] ?? 0) || 1; - - return { width, height }; -} diff --git a/apps/halidoscope/frontend/src/utils/graph.ts b/apps/halidoscope/frontend/src/utils/graph.ts index 50b28b8e9f71..e6fe770f78a6 100644 --- a/apps/halidoscope/frontend/src/utils/graph.ts +++ b/apps/halidoscope/frontend/src/utils/graph.ts @@ -1,33 +1,29 @@ import Dagre from "@dagrejs/dagre"; import type { Node, Edge } from "@xyflow/react"; -import { FuncStats, NodeData } from "../types"; -import { computeFuncSize } from "./func"; +import { FuncStats, NodeTypes } from "../types"; /** * Build xyflow nodes from the backend's funcs payload, which maps Halide func * @param funcs * @returns */ -export function buildNodes(funcs: Record): Node[] { +export function buildNodes( + funcs: Record, + type: NodeTypes, +): Node[] { return Object.entries(funcs).map(([name, stats]) => { - const { width, height } = computeFuncSize(stats); - return { id: name, - type: "funcCanvas", + type: type, position: { x: 0, y: 0, }, - data: { - ...stats, - width, - height, - }, + data: stats, style: { - width, - height, + width: stats.width, + height: stats.height, }, }; }); @@ -57,9 +53,9 @@ export function buildEdges(dagEdges: Record): Edge[] { } export function getLayoutedElements( - nodes: Node[], + nodes: Node[], edges: Edge[], -): { nodes: Node[]; edges: Edge[] } { +): { nodes: Node[]; edges: Edge[] } { const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: 80 }); diff --git a/apps/halidoscope/frontend/tsconfig.json b/apps/halidoscope/frontend/tsconfig.json index 9479566170ff..ed8feb33ec8d 100644 --- a/apps/halidoscope/frontend/tsconfig.json +++ b/apps/halidoscope/frontend/tsconfig.json @@ -18,7 +18,12 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": [ + "./src/*" + ] + } }, "include": [ "src" diff --git a/apps/halidoscope/frontend/vite.config.ts b/apps/halidoscope/frontend/vite.config.ts index 429a2de5795c..66b2910e5716 100644 --- a/apps/halidoscope/frontend/vite.config.ts +++ b/apps/halidoscope/frontend/vite.config.ts @@ -8,7 +8,9 @@ const host = process.env.TAURI_DEV_HOST; // https://vite.dev/config/ export default defineConfig(async () => ({ plugins: [react(), tailwindcss()], - + resolve: { + tsconfigPaths: true, + }, // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors diff --git a/python_bindings/src/halide/halide_/PyTrace.cpp b/python_bindings/src/halide/halide_/PyTrace.cpp index 300a2b7c6fb9..6eb8db9b06de 100644 --- a/python_bindings/src/halide/halide_/PyTrace.cpp +++ b/python_bindings/src/halide/halide_/PyTrace.cpp @@ -332,6 +332,95 @@ class Trace { return result; } + // Returns the indices of all load packets, in order. + // Cached once at load time in Python to avoid iterating all packets per render. + std::vector load_indices() const { + std::vector result; + result.reserve(packets_.size() / 4); + for (size_t i = 0; i < packets_.size(); ++i) { + if (packets_[i].is_load()) { + result.push_back(i); + } + } + return result; + } + + // Returns the maximum store count and maximum load count per Func across all + // pixels. Runs entirely in C++ to avoid per-packet Python/pybind11 overhead. + // Result: dict keyed by qualified func name, each value a dict with: + // max_store_count: int + // max_load_count: int + py::dict compute_max_load_store_counts() const { + // Map unqualified name -> FuncStats* for packet lookup. + // (TracePacket.func is always the unqualified name; funcs_ keys are qualified.) + std::map unqualified_to_stats; + for (const auto &[name, stats] : funcs_) { + auto colon = name.rfind(':'); + std::string unqualified = (colon != std::string::npos) ? name.substr(colon + 1) : name; + unqualified_to_stats.emplace(unqualified, &stats); + } + + struct FuncAccum { + std::vector store_counts; + std::vector load_counts; + int32_t width; + int32_t height; + int32_t min_x; + int32_t min_y; + }; + + std::map accum; + for (const auto &[qualified, stats] : funcs_) { + if (stats.min_coords.empty() || stats.max_coords.empty()) continue; + const int32_t width = stats.max_coords[0] - stats.min_coords[0]; + const int32_t height = + (stats.min_coords.size() > 1 && stats.max_coords.size() > 1) ? stats.max_coords[1] - stats.min_coords[1] : 1; + if (width <= 0 || height <= 0) continue; + accum[qualified] = FuncAccum{ + std::vector(height * width, 0), + std::vector(height * width, 0), + width, + height, + stats.min_coords[0], + (stats.min_coords.size() > 1) ? stats.min_coords[1] : 0, + }; + } + + for (const auto &pkt : packets_) { + if (!pkt.is_load_or_store()) continue; + + auto stats_it = unqualified_to_stats.find(pkt.func); + if (stats_it == unqualified_to_stats.end()) continue; + + auto accum_it = accum.find(stats_it->second->name); + if (accum_it == accum.end()) continue; + + FuncAccum &fa = accum_it->second; + const int32_t n_lanes = std::max(1, (int32_t)pkt.type_lanes); + const int32_t dims_per_lane = (int32_t)pkt.coordinates.size() / n_lanes; + int32_t *arr = pkt.is_store() ? fa.store_counts.data() : fa.load_counts.data(); + + for (int32_t l = 0; l < n_lanes; ++l) { + const int32_t x = pkt.coordinates[l] - fa.min_x; + const int32_t y = (dims_per_lane >= 2) ? pkt.coordinates[n_lanes + l] - fa.min_y : -fa.min_y; + if (x >= 0 && x < fa.width && y >= 0 && y < fa.height) { + arr[y * fa.width + x]++; + } + } + } + + py::dict result; + for (const auto &[qualified, fa] : accum) { + const int32_t max_store = *std::max_element(fa.store_counts.begin(), fa.store_counts.end()); + const int32_t max_load = *std::max_element(fa.load_counts.begin(), fa.load_counts.end()); + py::dict entry; + entry["max_store_count"] = max_store; + entry["max_load_count"] = max_load; + result[py::cast(qualified)] = entry; + } + return result; + } + std::string dag_as_dot() const { std::ostringstream ss; ss << "digraph dag {\n"; @@ -410,20 +499,34 @@ class Trace { static void update_stats_inline(const halide_trace_packet_t *pkt, FuncStats &stats) { - // Update coordinate ranges using the helper method + // Update coordinate ranges using the helper method. + // Coordinates are dim-major: [x0..xL, y0..yL, c0..cL] where L = type.lanes. + // pkt->dimensions = logical_dims * lanes, so we must stride by lanes to get + // the correct coordinate for each logical dimension. if (pkt->dimensions > 0) { const int *coords = pkt->coordinates(); + const int n_lanes = std::max(1, static_cast(pkt->type.lanes)); + const int logical_dims = pkt->dimensions / n_lanes; if (stats.min_coords.empty()) { - stats.min_coords.resize(pkt->dimensions); - stats.max_coords.resize(pkt->dimensions); - for (int i = 0; i < pkt->dimensions; ++i) { - stats.min_coords[i] = coords[i]; - stats.max_coords[i] = coords[i] + 1; + stats.min_coords.resize(logical_dims); + stats.max_coords.resize(logical_dims); + for (int d = 0; d < logical_dims; ++d) { + int mn = coords[d * n_lanes]; + int mx = coords[d * n_lanes] + 1; + for (int l = 1; l < n_lanes; ++l) { + mn = std::min(mn, coords[d * n_lanes + l]); + mx = std::max(mx, coords[d * n_lanes + l] + 1); + } + stats.min_coords[d] = mn; + stats.max_coords[d] = mx; } } else { - for (int i = 0; i < pkt->dimensions && i < static_cast(stats.min_coords.size()); ++i) { - stats.min_coords[i] = std::min(stats.min_coords[i], coords[i]); - stats.max_coords[i] = std::max(stats.max_coords[i], coords[i] + 1); + for (int d = 0; d < logical_dims && d < static_cast(stats.min_coords.size()); ++d) { + for (int l = 0; l < n_lanes; ++l) { + const int coord = coords[d * n_lanes + l]; + stats.min_coords[d] = std::min(stats.min_coords[d], coord); + stats.max_coords[d] = std::max(stats.max_coords[d], coord + 1); + } } } } @@ -547,7 +650,9 @@ void define_trace(py::module &m) { .def_property_readonly("packets", &Trace::packets) .def("filter_loads_stores", &Trace::filter_loads_stores) .def("store_indices", &Trace::store_indices) - .def("dag_as_dot", &Trace::dag_as_dot); + .def("load_indices", &Trace::load_indices) + .def("dag_as_dot", &Trace::dag_as_dot) + .def("compute_max_load_store_counts", &Trace::compute_max_load_store_counts); } } // namespace Halide::PythonBindings From 6b90a9c8802b4cc6a3c92b8c4f1c631693c753eb Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Fri, 19 Jun 2026 16:17:00 -0700 Subject: [PATCH 11/67] feat: Migrate to Rust backend and add Histograms. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/frontend/.prettierignore | 4 + apps/halidoscope/frontend/.prettierrc | 1 + apps/halidoscope/frontend/package.json | 6 +- apps/halidoscope/frontend/pnpm-lock.yaml | 102 +++ .../halidoscope/frontend/src-tauri/Cargo.lock | 7 + .../halidoscope/frontend/src-tauri/Cargo.toml | 12 + .../frontend/src-tauri/src/commands.rs | 291 +++++++ .../halidoscope/frontend/src-tauri/src/lib.rs | 13 +- .../frontend/src-tauri/src/render.rs | 409 +++++++++ .../frontend/src-tauri/src/trace.rs | 800 ++++++++++++++++++ apps/halidoscope/frontend/src/App.css | 12 + apps/halidoscope/frontend/src/App.tsx | 94 +- .../src/components/controls/ControlPanel.tsx | 4 +- .../src/components/controls/ControlTabs.tsx | 26 +- .../components/controls/funcs/FuncsPanel.tsx | 40 +- .../controls/playback/PlaybackLegend.tsx | 81 -- .../controls/playback/PlaybackPanel.tsx | 53 -- .../controls/visualizations/FuncSelect.tsx | 62 ++ .../controls/visualizations/Histogram.tsx | 120 +++ .../controls/visualizations/PlaybackRate.tsx | 51 ++ .../visualizations/VisualizationsPanel.tsx | 88 ++ .../visualizations/VisualizationsSelect.tsx | 71 ++ .../frontend/src/components/shared/Canvas.tsx | 20 +- .../src/components/shared/HandleCircle.tsx | 10 +- .../src/components/views/ViewTabs.tsx | 26 - .../components/views/tracer/FuncCanvas.tsx | 330 +++----- .../src/components/views/tracer/FuncEdge.tsx | 18 - .../src/components/views/tracer/Tracer.tsx | 42 +- .../views/tracer/TracerTimeline.tsx | 214 ++--- .../frontend/src/hooks/canvas-registry.ts | 93 -- apps/halidoscope/frontend/src/hooks/trace.ts | 11 +- apps/halidoscope/frontend/src/state/func.ts | 2 +- apps/halidoscope/frontend/src/state/packet.ts | 3 + .../frontend/src/state/playback.ts | 4 +- .../frontend/src/state/visualization.ts | 9 + apps/halidoscope/frontend/src/types/index.ts | 80 +- apps/halidoscope/frontend/src/utils/api.ts | 99 ++- .../frontend/src/utils/constants.ts | 8 +- apps/halidoscope/frontend/src/utils/graph.ts | 16 +- 39 files changed, 2436 insertions(+), 896 deletions(-) create mode 100644 apps/halidoscope/frontend/.prettierignore create mode 100644 apps/halidoscope/frontend/.prettierrc create mode 100644 apps/halidoscope/frontend/src-tauri/src/commands.rs create mode 100644 apps/halidoscope/frontend/src-tauri/src/render.rs create mode 100644 apps/halidoscope/frontend/src-tauri/src/trace.rs delete mode 100644 apps/halidoscope/frontend/src/components/controls/playback/PlaybackLegend.tsx delete mode 100644 apps/halidoscope/frontend/src/components/controls/playback/PlaybackPanel.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/visualizations/FuncSelect.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/visualizations/PlaybackRate.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx delete mode 100644 apps/halidoscope/frontend/src/components/views/ViewTabs.tsx delete mode 100644 apps/halidoscope/frontend/src/components/views/tracer/FuncEdge.tsx delete mode 100644 apps/halidoscope/frontend/src/hooks/canvas-registry.ts create mode 100644 apps/halidoscope/frontend/src/state/packet.ts create mode 100644 apps/halidoscope/frontend/src/state/visualization.ts diff --git a/apps/halidoscope/frontend/.prettierignore b/apps/halidoscope/frontend/.prettierignore new file mode 100644 index 000000000000..0b01a51456dc --- /dev/null +++ b/apps/halidoscope/frontend/.prettierignore @@ -0,0 +1,4 @@ +node_modules +/src-tauri/ +.vscode +*.yaml diff --git a/apps/halidoscope/frontend/.prettierrc b/apps/halidoscope/frontend/.prettierrc new file mode 100644 index 000000000000..394f7d50fce3 --- /dev/null +++ b/apps/halidoscope/frontend/.prettierrc @@ -0,0 +1 @@ +{ "plugins": ["prettier-plugin-tailwindcss"] } diff --git a/apps/halidoscope/frontend/package.json b/apps/halidoscope/frontend/package.json index 32b6ff39931a..5d6bed9435bc 100644 --- a/apps/halidoscope/frontend/package.json +++ b/apps/halidoscope/frontend/package.json @@ -7,10 +7,12 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "tauri": "tauri" + "tauri": "tauri", + "format": "prettier --write ." }, "dependencies": { "@dagrejs/dagre": "^3.0.0", + "@observablehq/plot": "^0.6.17", "@tailwindcss/vite": "^4.3.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-cli": "^2.4.1", @@ -32,6 +34,8 @@ "@vitejs/plugin-react": "^4.6.0", "eslint": "^10.4.1", "eslint-plugin-react-hooks": "^7.1.1", + "prettier": "^3.8.4", + "prettier-plugin-tailwindcss": "^0.8.0", "typescript": "~5.8.3", "typescript-eslint": "^8.60.1", "vite": "^8.0.16" diff --git a/apps/halidoscope/frontend/pnpm-lock.yaml b/apps/halidoscope/frontend/pnpm-lock.yaml index b715b4b14f41..8e6dd792ac24 100644 --- a/apps/halidoscope/frontend/pnpm-lock.yaml +++ b/apps/halidoscope/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@dagrejs/dagre': specifier: ^3.0.0 version: 3.0.0 + '@observablehq/plot': + specifier: ^0.6.17 + version: 0.6.17 '@tailwindcss/vite': specifier: ^4.3.0 version: 4.3.0(vite@8.0.16(jiti@2.7.0)) @@ -69,6 +72,12 @@ importers: eslint-plugin-react-hooks: specifier: ^7.1.1 version: 7.1.1(eslint@10.4.1(jiti@2.7.0)) + prettier: + specifier: ^3.8.4 + version: 3.8.4 + prettier-plugin-tailwindcss: + specifier: ^0.8.0 + version: 0.8.0(prettier@3.8.4) typescript: specifier: ~5.8.3 version: 5.8.3 @@ -275,6 +284,10 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@observablehq/plot@0.6.17': + resolution: {integrity: sha512-/qaXP/7mc4MUS0s4cPPFASDRjtsWp85/TbfsciqDgU1HwYixbSbbytNuInD8AcTYC3xaxACgVX06agdfQy9W+g==} + engines: {node: '>=12'} + '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} @@ -1483,6 +1496,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + binary-search-bounds@2.0.5: + resolution: {integrity: sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==} + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} @@ -1803,6 +1819,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + interval-tree-1d@1.0.4: + resolution: {integrity: sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1814,6 +1833,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isoformat@0.2.1: + resolution: {integrity: sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -2003,6 +2025,66 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-plugin-tailwindcss@0.8.0: + resolution: {integrity: sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g==} + engines: {node: '>=20.19'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.8.4: + resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + engines: {node: '>=14'} + hasBin: true + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2483,6 +2565,12 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@observablehq/plot@0.6.17': + dependencies: + d3: 7.9.0 + interval-tree-1d: 1.0.4 + isoformat: 0.2.1 + '@oxc-project/types@0.133.0': {} '@radix-ui/number@1.1.1': {} @@ -3718,6 +3806,8 @@ snapshots: baseline-browser-mapping@2.10.33: {} + binary-search-bounds@2.0.5: {} + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -4057,6 +4147,10 @@ snapshots: internmap@2.0.3: {} + interval-tree-1d@1.0.4: + dependencies: + binary-search-bounds: 2.0.5 + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -4065,6 +4159,8 @@ snapshots: isexe@2.0.0: {} + isoformat@0.2.1: {} + jiti@2.7.0: {} jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.15)(react@19.2.6): @@ -4201,6 +4297,12 @@ snapshots: prelude-ls@1.2.1: {} + prettier-plugin-tailwindcss@0.8.0(prettier@3.8.4): + dependencies: + prettier: 3.8.4 + + prettier@3.8.4: {} + punycode@2.3.1: {} radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): diff --git a/apps/halidoscope/frontend/src-tauri/Cargo.lock b/apps/halidoscope/frontend/src-tauri/Cargo.lock index 1d477667488d..0c5baafe2628 100644 --- a/apps/halidoscope/frontend/src-tauri/Cargo.lock +++ b/apps/halidoscope/frontend/src-tauri/Cargo.lock @@ -548,6 +548,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "colorous" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e18bf7a165bf7028fde98609a0f1e8f7498d762a212598e6c891f6893556ec" + [[package]] name = "combine" version = "4.6.7" @@ -1112,6 +1118,7 @@ dependencies = [ name = "frontend" version = "0.1.0" dependencies = [ + "colorous", "serde", "serde_json", "tauri", diff --git a/apps/halidoscope/frontend/src-tauri/Cargo.toml b/apps/halidoscope/frontend/src-tauri/Cargo.toml index bfe44502d69b..9322723ac0d0 100644 --- a/apps/halidoscope/frontend/src-tauri/Cargo.toml +++ b/apps/halidoscope/frontend/src-tauri/Cargo.toml @@ -22,6 +22,18 @@ tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +colorous = "1.0.16" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-cli = "2.0.0" + +# `tauri dev` builds with the dev profile, whose default opt-level = 0 leaves the +# trace parser (a tight binary loop over millions of packets) badly unoptimized +# — multi-minute loads on large traces. Optimize codegen while keeping fast +# incremental builds and debuggability. This is the single biggest startup win. +[profile.dev] +opt-level = 3 + +# Dependencies are compiled once and cached, so always optimize them fully. +[profile.dev.package."*"] +opt-level = 3 diff --git a/apps/halidoscope/frontend/src-tauri/src/commands.rs b/apps/halidoscope/frontend/src-tauri/src/commands.rs new file mode 100644 index 000000000000..2ab46f79a4a2 --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/src/commands.rs @@ -0,0 +1,291 @@ +//! Frontend-facing API contract for Halidoscope. +//! +//! This module owns the types that cross the Tauri IPC boundary. + +use std::collections::{BTreeMap, HashMap}; +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use tauri::ipc::Response; +use tauri::State; + +use crate::render::{HeatmapState, RedundantState, RenderState}; +use crate::trace::Trace; + +/// How a Func's values are mapped to pixels. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RenderMode { + Grayscale, + Rgb, +} + +/// Which access type to visualize in a heatmap render. Variant names match the frontend's +/// `VisualizationMode` strings so they can be passed through without conversion. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum HeatmapMode { + #[serde(rename = "Store Frequency")] + Stores, + #[serde(rename = "Load Frequency")] + Loads, +} + +/// The default render mode inferred from a Func's channel count: 3 or 4 channels are treated as +/// color, everything else as grayscale. Shared by the metadata derivation and `render_at` so the +/// inferred default never diverges. +pub fn default_mode(channels: u32) -> RenderMode { + if channels == 3 || channels == 4 { + RenderMode::Rgb + } else { + RenderMode::Grayscale + } +} + +/// Per-Func metadata the frontend needs to size canvases and bound the scrub timeline. +#[derive(Debug, Clone, Serialize)] +pub struct FuncMeta { + pub name: String, + pub width: u32, + pub height: u32, + pub channels: u32, + pub default_mode: RenderMode, + /// Number of store events for this Func across the whole trace. + pub num_stores: u32, + /// Per-dimension coordinate extent, half-open `[min, max)`. Surfaced for the + /// funcs inspector panel. + pub min_coords: Vec, + pub max_coords: Vec, + pub min_value: Option, + pub max_value: Option, + pub max_store_count: i32, + pub max_load_count: i32, + pub max_redundant_count: i32, + /// Frequency distributions of per-pixel counts, indexed by count value (the `0` bin is + /// included). Length is the corresponding `max_*_count + 1`; empty when the Func has no + /// usable extent. Rendered directly as histograms by the frontend. + pub store_count_histogram: Vec, + pub load_count_histogram: Vec, + pub redundant_count_histogram: Vec, +} + +/// Top-level payload returned by `open_trace`. +#[derive(Debug, Clone, Serialize)] +pub struct TraceMeta { + pub funcs: Vec, + pub total_packets: u32, + pub dag_edges: BTreeMap>, + pub global_max_store_count: i32, + pub global_max_load_count: i32, + pub global_max_redundant_count: i32, +} + +impl TraceMeta { + /// Derives the frontend contract from a parsed trace. Funcs with no usable coordinate extent + /// are still listed (with zero dimensions) so the UI can surface them; the renderer simply + /// produces nothing for them. + pub fn from_trace(trace: &Trace) -> Self { + let mut global_max_store_count = 0; + let mut global_max_load_count = 0; + let mut global_max_redundant_count = 0; + + let funcs = trace + .funcs + .iter() + .map(|(name, stats)| { + let geom = trace.func_geometry(name); + let (width, height, channels) = match geom { + Some(g) => (g.width as u32, g.height as u32, g.channels as u32), + None => (0, 0, 1), + }; + let default_mode = default_mode(channels); + let stores = trace.func_store_indices(name); + let num_stores = stores.map(<[usize]>::len).unwrap_or(0) as u32; + + if stats.max_store_count > global_max_store_count { + global_max_store_count = stats.max_store_count; + } + if stats.max_load_count > global_max_load_count { + global_max_load_count = stats.max_load_count; + } + if stats.max_redundant_count > global_max_redundant_count { + global_max_redundant_count = stats.max_redundant_count; + } + + FuncMeta { + name: name.clone(), + width, + height, + channels, + default_mode, + num_stores, + min_coords: stats.min_coords.clone(), + max_coords: stats.max_coords.clone(), + min_value: stats.min_value, + max_value: stats.max_value, + max_store_count: stats.max_store_count, + max_load_count: stats.max_load_count, + max_redundant_count: stats.max_redundant_count, + store_count_histogram: stats.store_count_histogram.clone(), + load_count_histogram: stats.load_count_histogram.clone(), + redundant_count_histogram: stats.redundant_count_histogram.clone(), + } + }) + .collect(); + + let dag_edges = trace + .dag_edges + .iter() + .map(|(consumer, producers)| (consumer.clone(), producers.iter().cloned().collect())) + .collect(); + + TraceMeta { + funcs, + total_packets: trace.packets.len() as u32, + dag_edges, + global_max_store_count, + global_max_load_count, + global_max_redundant_count, + } + } +} + +// ── Tauri-managed state ─────────────────────────────────────────────────────── + +/// The currently loaded trace plus a per-Func render cache. The cache keeps each Func's +/// framebuffer warm across requests so forward scrubbing only applies the delta of new stores. +struct Loaded { + trace: Trace, + renderers: HashMap, + heatmap_renderers: HashMap, + redundant_renderers: HashMap, +} + +/// App-wide state managed by Tauri. A single trace is loaded at a time; opening a new one replaces +/// it (and drops the stale render cache). +#[derive(Default)] +pub struct AppState { + inner: Mutex>, +} + +// ── Commands ────────────────────────────────────────────────────────────────── + +/// Parses a `.hltrace` file and returns the metadata the frontend needs to set up canvases and +/// the scrub timeline. Replaces any previously loaded trace. +#[tauri::command] +pub fn open_trace(path: String, state: State) -> Result { + let trace = Trace::load_from_file(&path)?; + let meta = TraceMeta::from_trace(&trace); + + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + *guard = Some(Loaded { + trace, + renderers: HashMap::new(), + heatmap_renderers: HashMap::new(), + redundant_renderers: HashMap::new(), + }); + Ok(meta) +} + +/// Renders `func`'s framebuffer state at global timeline position `global_index` and returns it +/// as raw RGBA8 bytes (delivered to the frontend as an `ArrayBuffer`, bypassing JSON). `mode` +/// overrides the Func's inferred default when provided. Both scrubbing and playback drive this +/// single command. +#[tauri::command] +pub fn render_at( + func: String, + global_index: u32, + mode: Option, + state: State, +) -> Result { + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + let loaded = guard.as_mut().ok_or("no trace loaded")?; + // Split the borrow so the trace can be read while a renderer is mutated. + let Loaded { + trace, renderers, .. + } = loaded; + + // Get or lazily build this Func's render state. + if !renderers.contains_key(&func) { + let rs = RenderState::new(trace, &func) + .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; + renderers.insert(func.clone(), rs); + } + let renderer = renderers.get_mut(&func).expect("just inserted"); + + // Resolve the global timeline index into a store count: how many of this Func's stores have + // occurred by `global_index` (inclusive). + let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); + let k = store_indices.partition_point(|&p| p <= global_index as usize); + renderer.seek(trace, store_indices, k); + + let mode = mode.unwrap_or_else(|| default_mode(renderer.channels() as u32)); + Ok(Response::new(renderer.to_rgba(mode))) +} + +/// Renders a heatmap of store or load counts for `func` up to `global_index` and returns raw RGBA8 +/// bytes. Mirrors `render_at` — forward seeks apply only the new events; backward seeks clear and +/// replay. Counts are normalized against the per-Func full-trace maximum so the color scale is stable. +#[tauri::command] +pub fn render_heatmap( + func: String, + global_index: u32, + mode: HeatmapMode, + state: State, +) -> Result { + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + let loaded = guard.as_mut().ok_or("no trace loaded")?; + let Loaded { + trace, + heatmap_renderers, + .. + } = loaded; + + if !heatmap_renderers.contains_key(&func) { + let hs = HeatmapState::new(trace, &func, mode) + .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; + heatmap_renderers.insert(func.clone(), hs); + } + let hs = heatmap_renderers.get_mut(&func).expect("just inserted"); + + let event_indices = match mode { + HeatmapMode::Stores => trace.func_store_indices(&func).unwrap_or(&[]), + HeatmapMode::Loads => trace.func_load_indices(&func).unwrap_or(&[]), + }; + let k = event_indices.partition_point(|&p| p <= global_index as usize); + hs.seek(trace, event_indices, k, mode); + + Ok(Response::new(hs.to_rgba())) +} + +/// Renders a heatmap of redundant store counts for `func` up to `global_index` and returns raw +/// RGBA8 bytes. A store is redundant when it writes the same value to a location that already holds +/// that value. Counts are normalized against the per-Func full-trace maximum so the scale is stable +/// while scrubbing. Pixels with zero redundant stores are black; positive counts map through the +/// Reds colormap. +#[tauri::command] +pub fn render_redundant( + func: String, + global_index: u32, + state: State, +) -> Result { + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + let loaded = guard.as_mut().ok_or("no trace loaded")?; + let Loaded { + trace, + redundant_renderers, + .. + } = loaded; + + if !redundant_renderers.contains_key(&func) { + let rs = RedundantState::new(trace, &func) + .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; + redundant_renderers.insert(func.clone(), rs); + } + let rs = redundant_renderers.get_mut(&func).expect("just inserted"); + + let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); + let k = store_indices.partition_point(|&p| p <= global_index as usize); + rs.seek(trace, store_indices, k); + + Ok(Response::new(rs.to_rgba())) +} diff --git a/apps/halidoscope/frontend/src-tauri/src/lib.rs b/apps/halidoscope/frontend/src-tauri/src/lib.rs index ebeabfbbf9d2..90cd43dfb853 100644 --- a/apps/halidoscope/frontend/src-tauri/src/lib.rs +++ b/apps/halidoscope/frontend/src-tauri/src/lib.rs @@ -1,3 +1,7 @@ +pub mod commands; +pub mod render; +pub mod trace; + // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn get_cwd() -> Result { @@ -19,7 +23,14 @@ pub fn run() { Ok(()) }) .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![get_cwd]) + .manage(commands::AppState::default()) + .invoke_handler(tauri::generate_handler![ + get_cwd, + commands::open_trace, + commands::render_at, + commands::render_heatmap, + commands::render_redundant + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/apps/halidoscope/frontend/src-tauri/src/render.rs b/apps/halidoscope/frontend/src-tauri/src/render.rs new file mode 100644 index 000000000000..88df07ab572d --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/src/render.rs @@ -0,0 +1,409 @@ +//! Framebuffer rendering for Halidoscope. +//! +//! A `RenderState` holds the accumulated pixel state for a single Func after applying the first +//! `applied_k` of its store events, plus everything needed to normalize and emit RGBA. It is +//! deliberately decoupled from `Trace`: the packets and the Func's store-index list are passed +//! into `seek`, so the state can live in Tauri-managed state alongside the (separately owned) +//! parsed trace without a self-referential borrow. +use ::colorous; + +use crate::commands::{HeatmapMode, RenderMode}; +use crate::trace::{pixel_xy, FuncGeometry, Trace, TracePacket}; + +pub struct RenderState { + geom: FuncGeometry, + min_v: f64, + max_v: f64, + /// Latest normalized intensity per (pixel, channel), row-major with the channel as the minor + /// axis: `framebuffer[(y * width + x) * channels + c]`. Length is `width * height * channels`. + /// Unwritten cells stay 0 (black). + framebuffer: Vec, + /// Number of this Func's store events currently reflected in `framebuffer`. + applied_k: usize, +} + +impl RenderState { + /// Builds an empty render state for `func`, or `None` if the Func has no usable geometry (no + /// coordinate extent / zero area). + pub fn new(trace: &Trace, func: &str) -> Option { + let geom = trace.func_geometry(func)?; + let stats = trace.funcs.get(func)?; + let min_v = stats.min_value.unwrap_or(0.0); + let max_v = stats.max_value.unwrap_or(255.0); + let framebuffer = vec![0u8; geom.width * geom.height * geom.channels]; + + Some(Self { + geom, + min_v, + max_v, + framebuffer, + applied_k: 0, + }) + } + + /// Brings the framebuffer to the state after the first `target_k` stores. Forward seeks apply + /// only the delta (`applied_k..target_k`); backward seeks clear and replay from zero. + /// `store_indices` is the Func's global packet-index list (from `Trace::func_store_indices`). + pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + + if target_k < self.applied_k { + // Rewind: no per-pixel history is kept, so reset and replay forward. + self.framebuffer.iter_mut().for_each(|b| *b = 0); + self.applied_k = 0; + } + + for &global_idx in &store_indices[self.applied_k..target_k] { + self.apply_store(&trace.packets[global_idx]); + } + self.applied_k = target_k; + } + + /// Writes one store packet's lanes into the framebuffer (last write wins). + fn apply_store(&mut self, pkt: &TracePacket) { + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + let FuncGeometry { + width, + height, + channels, + min_x, + min_y, + min_c, + .. + } = self.geom; + + for lane in 0..n_lanes { + let Some(v) = pkt.decoded_value(lane) else { + continue; + }; + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= width || y as usize >= height { + continue; + } + // Channel is logical dim 2 when present; otherwise the single plane 0. + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + if c < 0 || c as usize >= channels { + continue; + } + let idx = (y as usize * width + x as usize) * channels + c as usize; + self.framebuffer[idx] = self.normalize(v); + } + } + + /// Maps a decoded value to a [0, 255] intensity. + #[inline] + fn normalize(&self, v: f64) -> u8 { + if self.max_v > self.min_v { + (255.0 * (v - self.min_v) / (self.max_v - self.min_v)).clamp(0.0, 255.0) as u8 + } else { + 128 + } + } + + /// Produces a `width * height * 4` RGBA8 buffer ready for `putImageData`. `Rgb` maps planes + /// 0/1/2 to R/G/B (missing planes are 0); `Grayscale` replicates plane 0 across R/G/B. + /// Alpha is always opaque. + pub fn to_rgba(&self, mode: RenderMode) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + let pixels = width * height; + let mut out = vec![0u8; pixels * 4]; + + // Hoist the mode branch and the channel-count checks outside the pixel loop so each + // inner loop is branch-free and LLVM can auto-vectorize it. + let fb = &self.framebuffer; + match mode { + RenderMode::Grayscale => { + for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { + let v = src[0]; + chunk[0] = v; + chunk[1] = v; + chunk[2] = v; + chunk[3] = 255; + } + } + RenderMode::Rgb => { + if channels >= 3 { + for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { + chunk[0] = src[0]; + chunk[1] = src[1]; + chunk[2] = src[2]; + chunk[3] = 255; + } + } else if channels == 2 { + for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { + chunk[0] = src[0]; + chunk[1] = src[1]; + chunk[2] = 0; + chunk[3] = 255; + } + } else { + for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { + chunk[0] = src[0]; + chunk[1] = 0; + chunk[2] = 0; + chunk[3] = 255; + } + } + } + } + out + } + + /// Number of channel planes (logical dim 2 extent, or 1). + pub fn channels(&self) -> usize { + self.geom.channels + } +} + +// ── Redundant computation rendering ────────────────────────────────────────── + +/// Accumulated per-pixel redundant-store counts for one Func. A store to pixel (x, y, c) is +/// redundant when it writes the same bit-pattern that was last stored there. The full-trace max +/// redundant count (pre-computed at parse time) is used for normalization so the color scale is +/// stable across the entire scrub range. +pub struct RedundantState { + geom: FuncGeometry, + /// Last value stored per (pixel × channel), flat row-major: + /// `last_values[(y * width + x) * channels + c]`. + /// `None` = no store has landed here yet. + last_values: Vec>, + /// Redundant-store count per spatial pixel, indexed by `y * width + x`. + redundant_counts: Vec, + applied_k: usize, +} + +impl RedundantState { + /// Builds an empty redundant state for `func`, or `None` if the Func has no usable geometry. + pub fn new(trace: &Trace, func: &str) -> Option { + let geom = trace.func_geometry(func)?; + let n_pixels = geom.width * geom.height; + Some(Self { + geom, + last_values: vec![None; n_pixels * geom.channels], + redundant_counts: vec![0i32; n_pixels], + applied_k: 0, + }) + } + + fn reset(&mut self) { + self.last_values.iter_mut().for_each(|v| *v = None); + self.redundant_counts.iter_mut().for_each(|c| *c = 0); + self.applied_k = 0; + } + + /// Seeks to the state after the first `target_k` store events. Forward seeks apply only the + /// delta; backward seeks reset and replay from zero. + pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + if target_k < self.applied_k { + self.reset(); + } + for &global_idx in &store_indices[self.applied_k..target_k] { + self.apply_store(&trace.packets[global_idx]); + } + self.applied_k = target_k; + } + + fn apply_store(&mut self, pkt: &TracePacket) { + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + let FuncGeometry { + width, + height, + channels, + min_x, + min_y, + min_c, + .. + } = self.geom; + + for lane in 0..n_lanes { + let Some(v) = pkt.decoded_value(lane) else { + continue; + }; + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= width || y as usize >= height { + continue; + } + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + if c < 0 || c as usize >= channels { + continue; + } + let val_idx = (y as usize * width + x as usize) * channels + c as usize; + let pixel_idx = y as usize * width + x as usize; + let v_bits = v.to_bits(); + if let Some(prev_bits) = self.last_values[val_idx] { + if prev_bits == v_bits { + self.redundant_counts[pixel_idx] += 1; + } + } + self.last_values[val_idx] = Some(v_bits); + } + } + + /// Produces a `width × height × 4` RGBA8 buffer. Pixels with zero redundant stores are black; + /// pixels with one or more are mapped through the Reds colormap, normalized against the + /// per-Func full-trace maximum so the scale is stable while scrubbing. + pub fn to_rgba(&self) -> Vec { + let FuncGeometry { + width, + height, + max_redundant_count, + .. + } = self.geom; + + let gradient = colorous::INFERNO; + let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { + let c = gradient.eval_continuous(i as f64 / 255.0); + [c.r, c.g, c.b] + }); + let scale = if max_redundant_count > 0 { + 255.0 / max_redundant_count as f64 + } else { + 0.0 + }; + + let mut out = vec![0u8; width * height * 4]; + for (chunk, &count) in out.chunks_exact_mut(4).zip(self.redundant_counts.iter()) { + if count > 0 { + let ti = (count as f64 * scale) as usize; + let [r, g, b] = lut[ti.min(255)]; + chunk[0] = r; + chunk[1] = g; + chunk[2] = b; + } + chunk[3] = 255; + } + out + } +} + +// ── Heatmap rendering ───────────────────────────────────────────────────────── + +/// Accumulated per-pixel event counts for one Func, seekable along the global timeline. Mirrors +/// `RenderState` but tracks a count per pixel instead of the latest normalized value. Forward +/// seeks apply only the new events; backward seeks clear and replay. The full-trace max count +/// (pre-computed at parse time and stored in `FuncGeometry`) is used for normalization so the +/// color scale is stable across the entire scrub range. +pub struct HeatmapState { + geom: FuncGeometry, + mode: HeatmapMode, + counts: Vec, + applied_k: usize, +} + +impl HeatmapState { + /// Builds an empty heatmap state for `func`, or `None` if the Func has no usable geometry. + pub fn new(trace: &Trace, func: &str, mode: HeatmapMode) -> Option { + let geom = trace.func_geometry(func)?; + let counts = vec![0i32; geom.width * geom.height]; + Some(Self { + geom, + mode, + counts, + applied_k: 0, + }) + } + + /// Seeks to the state after the first `target_k` events of `new_mode`. If `new_mode` differs + /// from the cached mode the counts are cleared first. `event_indices` must be the Func's + /// global packet-index list for that mode. + pub fn seek( + &mut self, + trace: &Trace, + event_indices: &[usize], + target_k: usize, + new_mode: HeatmapMode, + ) { + if new_mode != self.mode { + self.counts.iter_mut().for_each(|c| *c = 0); + self.applied_k = 0; + self.mode = new_mode; + } + + let target_k = target_k.min(event_indices.len()); + if target_k < self.applied_k { + self.counts.iter_mut().for_each(|c| *c = 0); + self.applied_k = 0; + } + + for &idx in &event_indices[self.applied_k..target_k] { + self.increment_pixel(&trace.packets[idx]); + } + self.applied_k = target_k; + } + + fn increment_pixel(&mut self, pkt: &TracePacket) { + let FuncGeometry { + width, + height, + min_x, + min_y, + .. + } = self.geom; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for l in 0..n_lanes { + let (x, y) = pixel_xy(pkt, l, n_lanes, dims_per_lane, min_x, min_y); + if x >= 0 && y >= 0 && (x as usize) < width && (y as usize) < height { + self.counts[y as usize * width + x as usize] += 1; + } + } + } + + /// Produces a `width × height × 4` RGBA8 buffer with the inferno colormap applied. Counts are + /// normalized against the per-Func full-trace maximum so the scale is consistent as the + /// playhead moves. + pub fn to_rgba(&self) -> Vec { + let FuncGeometry { + width, + height, + max_store_count, + max_load_count, + .. + } = self.geom; + let max_count = match self.mode { + HeatmapMode::Stores => max_store_count, + HeatmapMode::Loads => max_load_count, + }; + + // Build a 256-entry LUT once before the pixel loop. Calling eval_continuous 256 times is + // negligible; doing it once per pixel at 750K+ pixels is the choppy inner loop culprit. + // 256 × 3 bytes = 768 bytes, fits entirely in L1 cache. + let gradient = colorous::INFERNO; + let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { + let c = gradient.eval_continuous(i as f64 / 255.0); + [c.r, c.g, c.b] + }); + let scale = if max_count > 0 { + 255.0 / max_count as f64 + } else { + 0.0 + }; + + let mut out = vec![0u8; width * height * 4]; + for (chunk, &count) in out.chunks_exact_mut(4).zip(self.counts.iter()) { + let ti = (count as f64 * scale) as usize; + let [r, g, b] = lut[ti.min(255)]; + chunk[0] = r; + chunk[1] = g; + chunk[2] = b; + chunk[3] = 255; + } + out + } +} diff --git a/apps/halidoscope/frontend/src-tauri/src/trace.rs b/apps/halidoscope/frontend/src-tauri/src/trace.rs new file mode 100644 index 000000000000..b582fe8f35b8 --- /dev/null +++ b/apps/halidoscope/frontend/src-tauri/src/trace.rs @@ -0,0 +1,800 @@ +use std::collections::{BTreeMap, BTreeSet, HashMap}; + +// ── Type system ─────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeCode { + Int, + Uint, + Float, + Handle, + BFloat, + Unknown(u8), +} + +impl TypeCode { + fn from_u8(v: u8) -> Self { + match v { + 0 => Self::Int, + 1 => Self::Uint, + 2 => Self::Float, + 3 => Self::Handle, + 4 => Self::BFloat, + other => Self::Unknown(other), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HalideType { + pub code: TypeCode, + pub bits: u8, + pub lanes: u16, +} + +impl HalideType { + // Obtain the number of bytes for a single scalar element (i.e., one lane) + // of a packet's value. For sub-byte types, this rounds up to the nearest + // whole byte. + pub fn elem_bytes(self) -> usize { + (self.bits as usize + 7) / 8 + } + + // Obtain the number of bytes for the entire value of a packet. + // This is the product of the number of lanes and the size of each lane. + pub fn value_bytes(self) -> usize { + self.lanes as usize * self.elem_bytes() + } +} + +// ── Event codes ─────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EventCode { + Load, + Store, + BeginRealization, + EndRealization, + Produce, + EndProduce, + Consume, + EndConsume, + BeginPipeline, + EndPipeline, + Tag, + Unknown(i32), +} + +impl EventCode { + fn from_i32(v: i32) -> Self { + match v { + 0 => Self::Load, + 1 => Self::Store, + 2 => Self::BeginRealization, + 3 => Self::EndRealization, + 4 => Self::Produce, + 5 => Self::EndProduce, + 6 => Self::Consume, + 7 => Self::EndConsume, + 8 => Self::BeginPipeline, + 9 => Self::EndPipeline, + 10 => Self::Tag, + other => Self::Unknown(other), + } + } +} + +// ── Parsed packet ───────────────────────────────────────────────────────────── + +#[derive(Debug, Clone)] +pub struct TracePacket { + pub id: i32, + pub event: EventCode, + pub parent_id: i32, + pub value_index: i32, + pub type_: HalideType, + /// Coordinates in dim-major / lane-minor order: + /// [x₀..xₙ, y₀..yₙ, c₀..cₙ] where n = type_.lanes. + pub coordinates: Vec, + pub value: Vec, + pub func: String, + pub trace_tag: String, +} + +impl TracePacket { + pub fn is_load(&self) -> bool { + self.event == EventCode::Load + } + + pub fn is_store(&self) -> bool { + self.event == EventCode::Store + } + + pub fn is_load_or_store(&self) -> bool { + self.is_load() || self.is_store() + } + + /// Decodes lane `lane` of this packet's value into an `f64`. Returns `None` + /// when the type isn't a decodable numeric (handle/unknown/odd bit width) + /// or when the lane runs past the value bytes. All numeric types collapse to + /// `f64` so callers have a single comparable scalar. + pub fn decoded_value(&self, lane: usize) -> Option { + let elem_bytes = self.type_.elem_bytes(); + if elem_bytes == 0 { + return None; + } + let off = lane * elem_bytes; + if off + elem_bytes > self.value.len() { + return None; + } + let s = &self.value[off..]; + match (self.type_.code, self.type_.bits) { + (TypeCode::Float, 32) => Some(f32::from_le_bytes(s[..4].try_into().unwrap()) as f64), + (TypeCode::Float, 64) => Some(f64::from_le_bytes(s[..8].try_into().unwrap())), + (TypeCode::Int, 8) => Some(s[0] as i8 as f64), + (TypeCode::Int, 16) => Some(i16::from_le_bytes(s[..2].try_into().unwrap()) as f64), + (TypeCode::Int, 32) => Some(i32::from_le_bytes(s[..4].try_into().unwrap()) as f64), + (TypeCode::Int, 64) => Some(i64::from_le_bytes(s[..8].try_into().unwrap()) as f64), + (TypeCode::Uint, 8) => Some(s[0] as f64), + (TypeCode::Uint, 16) => Some(u16::from_le_bytes(s[..2].try_into().unwrap()) as f64), + (TypeCode::Uint, 32) => Some(u32::from_le_bytes(s[..4].try_into().unwrap()) as f64), + (TypeCode::Uint, 64) => Some(u64::from_le_bytes(s[..8].try_into().unwrap()) as f64), + // bfloat16 is the upper 16 bits of an IEEE f32; reconstruct by shifting left 16. + (TypeCode::BFloat, 16) => { + let bits = u16::from_le_bytes(s[..2].try_into().unwrap()); + Some(f32::from_bits((bits as u32) << 16) as f64) + } + _ => None, + } + } +} + +// ── Per-Func statistics ─────────────────────────────────────────────────────── + +#[derive(Debug, Clone)] +pub struct FuncStats { + pub name: String, + pub min_coords: Vec, + pub max_coords: Vec, + pub min_value: Option, + pub max_value: Option, + pub max_store_count: i32, + pub max_load_count: i32, + pub max_redundant_count: i32, + /// Frequency distribution of per-pixel store counts: `hist[k]` is the number of pixel + /// locations stored exactly `k` times, for `k` in `0..=max_store_count`. The `0` bin is + /// included. Empty when the Func has no usable extent. + pub store_count_histogram: Vec, + pub load_count_histogram: Vec, + pub redundant_count_histogram: Vec, +} + +impl Default for FuncStats { + fn default() -> Self { + Self { + name: String::new(), + min_coords: vec![], + max_coords: vec![], + min_value: None, + max_value: None, + max_store_count: 0, + max_load_count: 0, + max_redundant_count: 0, + store_count_histogram: vec![], + load_count_histogram: vec![], + redundant_count_histogram: vec![], + } + } +} + +/// Full spatial layout of a Func: pixel dimensions plus the channel axis +/// (logical dim 2). Single source of truth for geometry, shared by the +/// renderer and by frontend-metadata derivation, so canvas sizing can never +/// disagree between them. All extents use the half-open `[min, max)` +/// convention that the coordinate accumulation establishes. +#[derive(Debug, Clone, Copy)] +pub struct FuncGeometry { + pub width: usize, + pub height: usize, + pub channels: usize, + pub min_x: i32, + pub min_y: i32, + pub min_c: i32, + pub max_store_count: i32, + pub max_load_count: i32, + pub max_redundant_count: i32, +} + +// ── Complete trace ──────────────────────────────────────────────────────────── + +// Note: We use BTreeMaps for deterministic iteration order here. We could +// consider switching to HashMaps to get O(1) lookups if we find funcs lookup +// start to become a bottleneck. +pub struct Trace { + pub packets: Vec, + pub funcs: BTreeMap, + pub pipelines: BTreeMap, + pub dag_edges: BTreeMap>, + pub store_indices_by_func: BTreeMap>, + pub load_indices_by_func: BTreeMap>, +} + +// ── Binary parsing helpers ──────────────────────────────────────────────────── + +// halide_trace_packet_t fixed header: 7 × 4 bytes = 28 bytes. +// u32 size @ 0 +// i32 id @ 4 +// u8 type.code @ 8 +// u8 type.bits @ 9 +// u16 type.lanes @ 10 +// i32 event @ 12 +// i32 parent_id @ 16 +// i32 value_index @ 20 +// i32 dimensions @ 24 +// +// Immediately after the header: +// i32 coordinates[dimensions] +// u8 value[type.lanes * ceil(type.bits / 8)] +// char func[] (null-terminated) +// char trace_tag[] (null-terminated; empty string if absent) +const HEADER_BYTES: usize = 28; + +// Helper functions to read little-endian integers from a byte buffer at a given offset. try_into() +// will convert the slice to a fixed-size [u8; N] array. We inline these for performance since they +// are called in the hot path of packet parsing. +#[inline] +fn u32_le(buf: &[u8], off: usize) -> u32 { + u32::from_le_bytes(buf[off..off + 4].try_into().unwrap()) +} + +#[inline] +fn i32_le(buf: &[u8], off: usize) -> i32 { + i32::from_le_bytes(buf[off..off + 4].try_into().unwrap()) +} + +#[inline] +fn u16_le(buf: &[u8], off: usize) -> u16 { + u16::from_le_bytes(buf[off..off + 2].try_into().unwrap()) +} + +/// Read a null-terminated C string. Returns `(string, bytes_consumed_including_null)`. +fn read_cstr(buf: &[u8]) -> (&str, usize) { + let null = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + (std::str::from_utf8(&buf[..null]).unwrap_or(""), null + 1) +} + +// ── Stats helpers called during packet parsing ──────────────────────────────── + +// Update the min/max coordinate vectors in FuncStats based on the coordinates seen in a given +// packet. The returned coordinates represent the range [min, max). +fn update_coord_range(pkt: &TracePacket, stats: &mut FuncStats) { + if pkt.coordinates.is_empty() { + return; + } + + let n_lanes = pkt.type_.lanes.max(1) as usize; + let logical_dims = pkt.coordinates.len() / n_lanes; + + // If this is the first load/store for this Func, initialize the min/max coordinate vectors. + // Otherwise, update the existing min/max values. + if stats.min_coords.is_empty() { + stats.min_coords.resize(logical_dims, 0); + stats.max_coords.resize(logical_dims, 0); + for d in 0..logical_dims { + let mut mn = pkt.coordinates[d * n_lanes]; + let mut mx = mn + 1; + for l in 1..n_lanes { + let c = pkt.coordinates[d * n_lanes + l]; + mn = mn.min(c); + mx = mx.max(c + 1); + } + stats.min_coords[d] = mn; + stats.max_coords[d] = mx; + } + } else { + let dims = logical_dims.min(stats.min_coords.len()); + for d in 0..dims { + for l in 0..n_lanes { + let c = pkt.coordinates[d * n_lanes + l]; + if c < stats.min_coords[d] { + stats.min_coords[d] = c; + } + if c + 1 > stats.max_coords[d] { + stats.max_coords[d] = c + 1; + } + } + } + } +} + +// Update the min/max value in FuncStats based on the value seen in a given packet. +fn update_value_range(pkt: &TracePacket, stats: &mut FuncStats) { + for i in 0..pkt.type_.lanes as usize { + if let Some(v) = pkt.decoded_value(i) { + match (stats.min_value, stats.max_value) { + (None, _) => { + stats.min_value = Some(v); + stats.max_value = Some(v); + } + (Some(mn), Some(mx)) => { + if v < mn { + stats.min_value = Some(v); + } + if v > mx { + stats.max_value = Some(v); + } + } + _ => {} + } + } + } +} + +// ── func_type_and_dim tag parsing ───────────────────────────────────────────── + +fn parse_func_type_and_dim( + qualified: &str, + trace_tag: &str, + funcs: &mut BTreeMap, +) { + // Format: "func_type_and_dim: [code bits lanes]{num_types} + // [min extent]{num_dims}" + let mut tokens = trace_tag.split_whitespace(); + tokens.next(); // consume "func_type_and_dim:" + + // Skip over the type descriptions, which we don't currently use. + // We could consider using them in the future to populate FuncStats.type if that'd be a + // useful addition. + let num_types: usize = match tokens.next().and_then(|s| s.parse().ok()) { + Some(n) => n, + None => return, + }; + for _ in 0..num_types * 3 { + tokens.next(); + } + + // Parse the dimension descriptions to extract the overall min and max + // coordinates for the Func. + let num_dims: usize = match tokens.next().and_then(|s| s.parse().ok()) { + Some(n) => n, + None => return, + }; + let mut min_coords = Vec::with_capacity(num_dims); + let mut max_coords = Vec::with_capacity(num_dims); + for _ in 0..num_dims { + let min: i32 = match tokens.next().and_then(|s| s.parse().ok()) { + Some(v) => v, + None => break, + }; + let extent: i32 = match tokens.next().and_then(|s| s.parse().ok()) { + Some(v) => v, + None => break, + }; + min_coords.push(min); + max_coords.push(min + extent); + } + + // Assign the declared extents wholesale, overwriting anything observed so far. + // Coordinate extents come from two sources that both write min_coords/max_coords: this tag + // (declared realization bounds) and update_coord_range (coords observed on Load/Store). + // + // In practice Halide emits this tag at pipeline start, before any load/store, so the common + //path is "tag seeds, accesses expand." + if !min_coords.is_empty() { + let entry = funcs.entry(qualified.to_owned()).or_default(); + entry.min_coords = min_coords; + entry.max_coords = max_coords; + } +} + +// ── Trace loading ───────────────────────────────────────────────────────────── + +impl Trace { + pub fn load_from_file(path: &str) -> Result { + let data = std::fs::read(path).map_err(|e| e.to_string())?; + Self::load_from_bytes(&data) + } + + pub fn load_from_bytes(data: &[u8]) -> Result { + let total = data.len(); + let mut pos = 0; + + let mut packets: Vec = Vec::new(); + let mut funcs: BTreeMap = BTreeMap::new(); + let mut pipelines: BTreeMap = BTreeMap::new(); + let mut dag_edges: BTreeMap> = BTreeMap::new(); + let mut store_indices_by_func: BTreeMap> = BTreeMap::new(); + let mut load_indices_by_func: BTreeMap> = BTreeMap::new(); + + // id -> pipeline name: propagated down the parent chain so every event + // in a pipeline can compute its qualified name. + let mut parent_to_pipeline: HashMap = HashMap::new(); + // id -> (event, qualified_name, parent_id): needed for DAG inference after + // all packets are parsed. + let mut id_to_info: HashMap = HashMap::new(); + // Loads we deferred for DAG inference. + let mut pending_loads: Vec<(String, i32)> = Vec::new(); + + while pos + HEADER_BYTES <= total { + let size = u32_le(data, pos) as usize; + if size < HEADER_BYTES || pos + size > total { + break; + } + + // ── Fixed header fields ─────────────────────────────────────────── + let id = i32_le(data, pos + 4); + let type_code = data[pos + 8]; + let type_bits = data[pos + 9]; + let type_lanes = u16_le(data, pos + 10); + let event = i32_le(data, pos + 12); + let parent_id = i32_le(data, pos + 16); + let value_index = i32_le(data, pos + 20); + let dimensions = i32_le(data, pos + 24) as usize; + + let type_ = HalideType { + code: TypeCode::from_u8(type_code), + bits: type_bits, + lanes: type_lanes, + }; + let ev = EventCode::from_i32(event); + let pkt_data = &data[pos..pos + size]; + + // ── Variable-length trailing fields ─────────────────────────────── + let coords_off = HEADER_BYTES; + let value_off = coords_off + dimensions * 4; + let value_len = type_.value_bytes(); + let func_off = value_off + value_len; + + let coords: Vec = (0..dimensions) + .map(|i| i32_le(pkt_data, coords_off + i * 4)) + .collect(); + + let value = if value_off + value_len <= pkt_data.len() { + pkt_data[value_off..value_off + value_len].to_vec() + } else { + vec![] + }; + + let (func_name, func_len) = if func_off < pkt_data.len() { + let (s, n) = read_cstr(&pkt_data[func_off..]); + (s.to_owned(), n) + } else { + (String::new(), 0) + }; + + let tag_off = func_off + func_len; + let trace_tag = if tag_off < pkt_data.len() { + let (s, _) = read_cstr(&pkt_data[tag_off..]); + s.to_owned() + } else { + String::new() + }; + + // ── Pipeline context propagation ────────────────────────────────── + match ev { + EventCode::BeginPipeline => { + pipelines.insert(id, func_name.clone()); + parent_to_pipeline.insert(id, func_name.clone()); + } + EventCode::EndPipeline => { + parent_to_pipeline.remove(&parent_id); + } + _ => { + if let Some(pl) = parent_to_pipeline.get(&parent_id).cloned() { + parent_to_pipeline.insert(id, pl); + } + } + } + + // ── Qualified name ──────────────────────────────────────────────── + let qualified = match parent_to_pipeline.get(&parent_id) { + Some(pl) if !pl.is_empty() => format!("{}:{}", pl, func_name), + _ => func_name.clone(), + }; + id_to_info.insert(id, (ev, qualified.clone(), parent_id)); + + // ── Build the packet ────────────────────────────────────────────── + let pkt = TracePacket { + id, + event: ev, + parent_id, + value_index, + type_, + coordinates: coords, + value, + func: func_name.clone(), + trace_tag: trace_tag.clone(), + }; + + // ── Update per-Func stats ───────────────────────────────────────── + // Coordinate extents are populated from two sources below: the func_type_and_dim tag + // (declared bounds) and Load/Store coords (observed bounds). Both write min_coords / + // max_coords; their interaction is order-dependent by design. + match ev { + EventCode::Tag if trace_tag.starts_with("func_type_and_dim:") => { + parse_func_type_and_dim(&qualified, &trace_tag, &mut funcs); + } + EventCode::BeginRealization => { + funcs.entry(qualified.clone()).or_default(); + } + EventCode::Load => { + // When we observe a load event, add its current index (equivalent to + // packets.len() before the push) to our BTreeMap of load indices for this Func. + load_indices_by_func + .entry(qualified.clone()) + .or_default() + .push(packets.len()); + + // Add the load event to the list of pending loads to support DAG inference. + pending_loads.push((func_name.clone(), parent_id)); + let stats = funcs.entry(qualified.clone()).or_default(); + + // Update the min/max coordinate and value ranges for this Func based on the + // current load packet. + update_coord_range(&pkt, stats); + update_value_range(&pkt, stats); + } + EventCode::Store => { + // When we observe a store event, add its current index (equivalent to + // packets.len() before the push) to our BTreeMap of store indices for this + // Func. + store_indices_by_func + .entry(qualified.clone()) + .or_default() + .push(packets.len()); + + // Update the min/max coordinate and value ranges for this Func based on the + // current store packet. + let stats = funcs.entry(qualified.clone()).or_default(); + update_coord_range(&pkt, stats); + update_value_range(&pkt, stats); + } + _ => {} + } + + packets.push(pkt); + pos += size; + } + + // ── DAG inference ───────────────────────────────────────────────────── + // Walk up the parent chain from each load to find the enclosing Produce + // event; that Produce's func is a producer of the loaded func. + for (func_name, load_parent_id) in &pending_loads { + let loaded_func = match parent_to_pipeline.get(load_parent_id) { + Some(pl) if !pl.is_empty() => format!("{}:{}", pl, func_name), + _ => func_name.clone(), + }; + + let mut current = *load_parent_id; + loop { + match id_to_info.get(¤t) { + Some((EventCode::Produce, producing_func, _)) => { + if loaded_func != *producing_func { + dag_edges + .entry(loaded_func.clone()) + .or_default() + .insert(producing_func.clone()); + } + break; + } + Some((_, _, next_parent)) => current = *next_parent, + None => break, + } + } + } + + // Compute max per-pixel store/load counts for each Func using the qualified-name index + // lists. We extract extents first (shared borrow) then write back (mut borrow) to keep the + // two borrows of `funcs` non-overlapping. + for (qualified, indices) in &store_indices_by_func { + let extents = funcs.get(qualified.as_str()).and_then(func_extents); + if let Some((w, h, min_x, min_y)) = extents { + let mut counts = vec![0i32; w * h]; + for &idx in indices { + let pkt = &packets[idx]; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for l in 0..n_lanes { + let (x, y) = pixel_xy(pkt, l, n_lanes, dims_per_lane, min_x, min_y); + if x >= 0 && y >= 0 && (x as usize) < w && (y as usize) < h { + counts[y as usize * w + x as usize] += 1; + } + } + } + if let Some(stats) = funcs.get_mut(qualified.as_str()) { + let (max, hist) = count_histogram(&counts); + stats.max_store_count = max; + stats.store_count_histogram = hist; + } + } + } + + for (qualified, indices) in &load_indices_by_func { + let extents = funcs.get(qualified.as_str()).and_then(func_extents); + if let Some((w, h, min_x, min_y)) = extents { + let mut counts = vec![0i32; w * h]; + for &idx in indices { + let pkt = &packets[idx]; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for l in 0..n_lanes { + let (x, y) = pixel_xy(pkt, l, n_lanes, dims_per_lane, min_x, min_y); + if x >= 0 && y >= 0 && (x as usize) < w && (y as usize) < h { + counts[y as usize * w + x as usize] += 1; + } + } + } + if let Some(stats) = funcs.get_mut(qualified.as_str()) { + let (max, hist) = count_histogram(&counts); + stats.max_load_count = max; + stats.load_count_histogram = hist; + } + } + } + + // Compute max per-pixel redundant store counts: replay all stores for each Func, tracking + // the last value written to each (x, y, channel). A store is redundant when the incoming + // value bit-matches the previously stored value at that location. + for (qualified, indices) in &store_indices_by_func { + let extents = funcs.get(qualified.as_str()).and_then(func_extents); + if let Some((w, h, min_x, min_y)) = extents { + let stats = funcs.get(qualified.as_str()).unwrap(); + let (channels, min_c) = if stats.min_coords.len() >= 3 { + ( + (stats.max_coords[2] - stats.min_coords[2]).max(1) as usize, + stats.min_coords[2], + ) + } else { + (1, 0) + }; + // None = no store has landed here yet; Some(bits) = last stored value as u64 bits. + let mut last_values = vec![None::; w * h * channels]; + let mut redundant_counts = vec![0i32; w * h]; + for &idx in indices { + let pkt = &packets[idx]; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for lane in 0..n_lanes { + let Some(v) = pkt.decoded_value(lane) else { + continue; + }; + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= w || y as usize >= h { + continue; + } + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + if c < 0 || c as usize >= channels { + continue; + } + let val_idx = (y as usize * w + x as usize) * channels + c as usize; + let pixel_idx = y as usize * w + x as usize; + let v_bits = v.to_bits(); + if let Some(prev_bits) = last_values[val_idx] { + if prev_bits == v_bits { + redundant_counts[pixel_idx] += 1; + } + } + last_values[val_idx] = Some(v_bits); + } + } + if let Some(stats) = funcs.get_mut(qualified.as_str()) { + let (max, hist) = count_histogram(&redundant_counts); + stats.max_redundant_count = max; + stats.redundant_count_histogram = hist; + } + } + } + + Ok(Self { + packets, + funcs, + pipelines, + dag_edges, + store_indices_by_func, + load_indices_by_func, + }) + } + + // ── Render-path accessors ───────────────────────────────────────────────── + + /// Global packet indices of `qualified`'s store events, in ascending order. `None` if the Func + /// emitted no stores. Use `partition_point(|&p| p <= g)` on the returned slice to turn a global + /// timeline index `g` into the number of stores that have occurred by that point. + pub fn func_store_indices(&self, qualified: &str) -> Option<&[usize]> { + self.store_indices_by_func.get(qualified).map(Vec::as_slice) + } + + pub fn func_load_indices(&self, qualified: &str) -> Option<&[usize]> { + self.load_indices_by_func.get(qualified).map(Vec::as_slice) + } + + /// Spatial layout for `qualified`, or `None` if it has no usable coordinate extent. Reuses + /// `func_extents` for pixel dims so the renderer and the metadata layer agree, and adds the + /// channel axis (logical dim 2). + pub fn func_geometry(&self, qualified: &str) -> Option { + let stats = self.funcs.get(qualified)?; + let (width, height, min_x, min_y) = func_extents(stats)?; + let (channels, min_c) = if stats.min_coords.len() >= 3 { + ( + (stats.max_coords[2] - stats.min_coords[2]).max(0) as usize, + stats.min_coords[2], + ) + } else { + (1, 0) + }; + Some(FuncGeometry { + width, + height, + channels: channels.max(1), + min_x, + min_y, + min_c, + max_store_count: stats.max_store_count, + max_load_count: stats.max_load_count, + max_redundant_count: stats.max_redundant_count, + }) + } +} + +// ── Shared geometry helpers ─────────────────────────────────────────────────── + +/// Builds a frequency histogram from per-pixel `counts`: `hist[k]` is the number of pixel +/// locations whose count is exactly `k`, for `k` in `0..=max`. The `0` bin is included so the +/// frontend can surface untouched locations. Counts are always non-negative. Returns +/// `(max_count, hist)`. +fn count_histogram(counts: &[i32]) -> (i32, Vec) { + let max = counts.iter().copied().max().unwrap_or(0); + let mut hist = vec![0u32; max as usize + 1]; + for &c in counts { + hist[c as usize] += 1; + } + (max, hist) +} + +/// Returns `(width, height, min_x, min_y)` for a Func, or `None` if the stats +/// have no coordinate information or produce a zero-area extent. +fn func_extents(stats: &FuncStats) -> Option<(usize, usize, i32, i32)> { + if stats.min_coords.is_empty() || stats.max_coords.is_empty() { + return None; + } + let width = (stats.max_coords[0] - stats.min_coords[0]) as usize; + let height = if stats.min_coords.len() > 1 { + (stats.max_coords[1] - stats.min_coords[1]) as usize + } else { + 1 + }; + if width == 0 || height == 0 { + return None; + } + let min_x = stats.min_coords[0]; + let min_y = if stats.min_coords.len() > 1 { + stats.min_coords[1] + } else { + 0 + }; + Some((width, height, min_x, min_y)) +} + +/// Returns the `(x, y)` canvas pixel for lane `l` of `pkt`, relative to the +/// Func's origin. Caller must bounds-check before indexing the canvas. +#[inline] +pub(crate) fn pixel_xy( + pkt: &TracePacket, + lane: usize, + n_lanes: usize, + dims_per_lane: usize, + min_x: i32, + min_y: i32, +) -> (i32, i32) { + let x = pkt.coordinates[lane] - min_x; + let y = if dims_per_lane >= 2 { + pkt.coordinates[n_lanes + lane] - min_y + } else { + -min_y + }; + (x, y) +} diff --git a/apps/halidoscope/frontend/src/App.css b/apps/halidoscope/frontend/src/App.css index 781722bcf57d..6575625aed1e 100644 --- a/apps/halidoscope/frontend/src/App.css +++ b/apps/halidoscope/frontend/src/App.css @@ -19,6 +19,18 @@ --zoom-level: 1; } +/* Chrome, Safari, Edge, Opera */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +input[type="number"] { + -moz-appearance: textfield; +} + @theme { --color-ps-primary: oklch(0.4423 0 0); --color-ps-secondary: oklch(0.2768 0 0); diff --git a/apps/halidoscope/frontend/src/App.tsx b/apps/halidoscope/frontend/src/App.tsx index e407063f1e67..2fdbdd42c3bc 100644 --- a/apps/halidoscope/frontend/src/App.tsx +++ b/apps/halidoscope/frontend/src/App.tsx @@ -1,92 +1,84 @@ import { invoke } from "@tauri-apps/api/core"; import { getMatches } from "@tauri-apps/plugin-cli"; +import { useSetAtom } from "jotai"; import * as React from "react"; -import ViewTabs from "@/components/views/ViewTabs"; -import type { FuncStats } from "@/types"; -import { CanvasRegistry } from "@/hooks/canvas-registry"; +import Tracer from "@/components/views/tracer/Tracer"; import { TraceContextProvider } from "@/hooks/trace"; -import { loadTracePath, deregisterTrace } from "@/utils/api"; +import type { FuncMeta } from "@/types"; +import { openTrace } from "@/utils/api"; import "./App.css"; +import { funcAtom } from "./state/func"; function App() { - const [sessionId, setSessionId] = React.useState(""); - const [funcs, setFuncs] = React.useState>({}); + const [funcs, setFuncs] = React.useState>({}); const [dagEdges, setDagEdges] = React.useState>({}); const [packetCount, setPacketCount] = React.useState(0); - const [canvasRegistry, setCanvasRegistry] = - React.useState(null); const [globalMaxStoreCount, setGlobalMaxStoreCount] = React.useState(0); const [globalMaxLoadCount, setGlobalMaxLoadCount] = React.useState(0); + const [globalMaxRedundantCount, setGlobalMaxRedundantCount] = + React.useState(0); - React.useEffect(() => { - const controller = new AbortController(); - // Track the loaded session ID locally so the cleanup closure always has - // the correct value even though sessionId state starts as "". - let loadedSessionId = ""; + const setActiveFunc = useSetAtom(funcAtom); + React.useEffect(() => { async function loadTraceFromCLI() { const matches = await getMatches(); const tracePath = matches.args.trace?.value; - if (typeof tracePath === "string") { - const resolved = tracePath.startsWith("/") - ? tracePath - : `${await invoke("get_cwd")}/${tracePath}`; + if (typeof tracePath !== "string") { + return; + } + + const resolved = tracePath.startsWith("/") + ? tracePath + : `${await invoke("get_cwd")}/${tracePath}`; - try { - const { - session_id, - funcs, - dag_edges, - num_packets, - global_max_store_count, - global_max_load_count, - } = await loadTracePath(resolved, controller.signal); + try { + const { + funcs, + total_packets, + dag_edges, + global_max_store_count, + global_max_load_count, + global_max_redundant_count, + } = await openTrace(resolved); - loadedSessionId = session_id; - setSessionId(session_id); - setFuncs(funcs); - setDagEdges(dag_edges); - setPacketCount(num_packets); - setGlobalMaxStoreCount(global_max_store_count); - setGlobalMaxLoadCount(global_max_load_count); - setCanvasRegistry(new CanvasRegistry()); - } catch (err) { - if ((err as Error).name !== "AbortError") { - console.error("Error loading trace from CLI: ", err); - } + const byName: Record = {}; + for (const func of funcs) { + byName[func.name] = func; } + + setFuncs(byName); + setDagEdges(dag_edges); + setPacketCount(total_packets); + setGlobalMaxStoreCount(global_max_store_count); + setGlobalMaxLoadCount(global_max_load_count); + setGlobalMaxRedundantCount(global_max_redundant_count); + setActiveFunc(funcs[0]?.name ?? ""); + } catch (err) { + console.error("Error loading trace from CLI: ", err); } } loadTraceFromCLI(); - - return () => { - controller.abort(); - - if (loadedSessionId) { - deregisterTrace(loadedSessionId).catch((err) => console.error(err)); - } - }; - }, []); + }, [setActiveFunc]); return ( -
- +
+
); diff --git a/apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx b/apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx index 5465372709a0..c1617787b4cd 100644 --- a/apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx +++ b/apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx @@ -6,10 +6,10 @@ interface ControlPanelProps { function ControlPanel({ setHidden }: ControlPanelProps) { return ( -
+
setHidden(checked === false)} diff --git a/apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx b/apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx index 062425b4ec24..92da02fe5071 100644 --- a/apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx +++ b/apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx @@ -1,37 +1,37 @@ import { Tabs } from "radix-ui"; import FuncsPanel from "@/components/controls/funcs/FuncsPanel"; -import PlaybackPanel from "@/components/controls/playback/PlaybackPanel"; -import { FuncStats } from "@/types"; +import VisualizationsPanel from "@/components/controls/visualizations/VisualizationsPanel"; +import { FuncMeta } from "@/types"; -function ControlTabs({ funcs }: { funcs: Record }) { +function ControlTabs({ funcs }: { funcs: Record }) { return (
-
-
+
+
- + Funcs - Playback + Visualizations - - + +
diff --git a/apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx b/apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx index 824d05b4dc47..48ea3345bbfb 100644 --- a/apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx +++ b/apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx @@ -1,11 +1,11 @@ import { useAtom } from "jotai"; import { Accordion } from "radix-ui"; -import type { FuncStats } from "@/types"; +import type { FuncMeta } from "@/types"; import { funcAtom } from "@/state/func"; interface FuncsPanelProps { - funcs: Record; + funcs: Record; } function FuncsPanel({ funcs }: FuncsPanelProps) { @@ -15,7 +15,7 @@ function FuncsPanel({ funcs }: FuncsPanelProps) { setFunc(value)} > @@ -25,7 +25,7 @@ function FuncsPanel({ funcs }: FuncsPanelProps) { value={func.name} className="group flex flex-col" > - + {func.name} - -
- + +
+ Minimum Coordinates - + ({func.min_coords.join(",")}) - + Maximum Coordinates - + ({func.max_coords.join(",")}) - + Minimum Value - + {func.min_value} - + Maximum Value - + {func.max_value} - + Maximum Store Count - - {func.max_store_count} + + {func.max_store_count.toLocaleString()} - + Maximum Load Count - - {func.max_load_count} + + {func.max_load_count.toLocaleString()}
diff --git a/apps/halidoscope/frontend/src/components/controls/playback/PlaybackLegend.tsx b/apps/halidoscope/frontend/src/components/controls/playback/PlaybackLegend.tsx deleted file mode 100644 index 46ac7ab2f65c..000000000000 --- a/apps/halidoscope/frontend/src/components/controls/playback/PlaybackLegend.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import * as d3 from "d3"; -import * as React from "react"; - -import { useTraceContext } from "@/hooks/trace"; -import type { PlaybackMode } from "@/state/playback"; - -const RAMP_HEIGHT = 16; - -function materializeColorRamp( - interpolator: (t: number) => string, - direction: "Forward" | "Reverse", - n: number, -): string[] { - const colors: string[] = []; - - for (let i = 0; i <= n; i++) { - colors.push( - d3 - .rgb(interpolator((direction === "Forward" ? i : n - i) / n)) - .formatHex(), - ); - } - - return colors; -} - -interface PlaybackLegendProps { - playbackMode: PlaybackMode; -} - -function PlaybackLegend({ playbackMode }: PlaybackLegendProps) { - const canvas = React.useRef(null); - const colors = materializeColorRamp( - playbackMode === "stores" ? d3.interpolateReds : d3.interpolateBlues, - "Forward", - 256, - ); - const { globalMaxStoreCount, globalMaxLoadCount } = useTraceContext(); - - const drawRamp = React.useCallback( - (ctx: CanvasRenderingContext2D) => { - ctx.clearRect(0, 0, canvas.current!.width, canvas.current!.height); - - for (let i = 0; i < colors.length; ++i) { - ctx.fillStyle = colors[i]; - ctx.fillRect(i, 0, 1, RAMP_HEIGHT); - } - }, - [colors], - ); - - React.useEffect(() => { - if (canvas.current) { - const ctx = canvas.current?.getContext("2d"); - - canvas.current.style.width = "100%"; - canvas.current.style.height = `${RAMP_HEIGHT}px`; - - if (ctx) { - drawRamp(ctx); - } - } - }, [drawRamp]); - - return ( -
- - {playbackMode === "stores" ? "Store Count →" : "Load Count →"} - - -
- 0 - - {playbackMode === "stores" ? globalMaxStoreCount : globalMaxLoadCount} - -
-
- ); -} - -export default PlaybackLegend; diff --git a/apps/halidoscope/frontend/src/components/controls/playback/PlaybackPanel.tsx b/apps/halidoscope/frontend/src/components/controls/playback/PlaybackPanel.tsx deleted file mode 100644 index f403e3982506..000000000000 --- a/apps/halidoscope/frontend/src/components/controls/playback/PlaybackPanel.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useAtom } from "jotai"; -import { RadioGroup } from "radix-ui"; - -import PlaybackLegend from "@/components/controls/playback/PlaybackLegend"; -import { playbackModeAtom, type PlaybackMode } from "@/state/playback"; - -const PLAYBACK_MODES = [ - { value: "normal", label: "Normal" }, - { value: "stores", label: "Stores" }, - { value: "loads", label: "Loads" }, -] as const; - -function PlaybackPanel() { - const [playbackMode, setPlaybackMode] = useAtom(playbackModeAtom); - - return ( -
-
- - setPlaybackMode(value as PlaybackMode)} - className="flex gap-3" - > - {PLAYBACK_MODES.map(({ value, label }) => ( -
- - - - -
- ))} -
-
- {playbackMode !== "normal" ? ( - - ) : null} -
- ); -} - -export default PlaybackPanel; diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/FuncSelect.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/FuncSelect.tsx new file mode 100644 index 000000000000..5ce4bb191e6a --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/FuncSelect.tsx @@ -0,0 +1,62 @@ +import { Label, Select } from "radix-ui"; +import { useAtom } from "jotai"; + +import { useTraceContext } from "@/hooks/trace"; +import { funcAtom } from "@/state/func"; + +function FuncSelect() { + const { funcs } = useTraceContext(); + const [activeFunc, setActiveFunc] = useAtom(funcAtom); + + return ( +
+ Selected Func + setActiveFunc(value)} + > + + + + + + + + + + + {Object.keys(funcs).map((func) => ( + + {func} + + ))} + + + +
+ ); +} + +export default FuncSelect; diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx new file mode 100644 index 000000000000..c7a66afc230a --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx @@ -0,0 +1,120 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import * as React from "react"; + +const RAMP_STOPS = 32; +const STOPS = Array.from({ length: RAMP_STOPS + 1 }, (_, i) => { + const t = i / RAMP_STOPS; + return { offset: t, color: d3.rgb(d3.interpolateInferno(t)).formatHex() }; +}); +const RAMP_DY = 12; + +interface HistogramProps { + data: { x: number; y: number }[]; + labels: { + x: string; + }; +} + +function Histogram({ data, labels }: HistogramProps) { + const ref = React.useRef(null); + const gradient = React.useRef(null); + const rampId = `histogram-ramp-${React.useId().replace(/:/g, "")}`; + + React.useEffect(() => { + if (!ref.current) { + return; + } + + const plot = Plot.plot({ + style: { + fontSize: "12px", + }, + marginBottom: 60, + y: { + grid: true, + label: "Pixel Count", + tickFormat: (value) => d3.format(".2s")(value), + }, + x: { + label: labels.x, + labelAnchor: "right", + labelArrow: "right", + tickSize: 0, + tickPadding: 24, + }, + color: { + scheme: "Inferno", + }, + marks: [ + Plot.barY(data, { x: "x", y: "y", fill: "x" }), + Plot.ruleY([0], { + stroke: `url(#${rampId})`, + strokeWidth: 8, + dy: RAMP_DY, + }), + ], + }); + + ref.current.append(plot); + + const xScale = plot.scale("x"); + if (xScale && gradient.current && data.length > 0) { + const bandwidth = xScale.bandwidth ?? 0; + const left = xScale.apply(data[0]!.x); + const right = xScale.apply(data[data.length - 1]!.x) + bandwidth; + + gradient.current.setAttribute("x1", String(left)); + gradient.current.setAttribute("x2", String(right)); + + // Plot draws the rule across the full frame; clip its rendered line(s) to the same + // footprint so the strip starts and ends with the bars rather than at the axes. The rule + // group is the only element stroked with our gradient, so we can find it by that url. + plot + .querySelector(`[stroke="url(#${rampId})"]`) + ?.querySelectorAll("line") + .forEach((line) => { + line.setAttribute("x1", String(left)); + line.setAttribute("x2", String(right)); + }); + } + + return () => { + plot.remove(); + }; + }, [data, labels, rampId]); + + return ( + <> + +
+ + ); +} + +export default Histogram; diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/PlaybackRate.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/PlaybackRate.tsx new file mode 100644 index 000000000000..3a8243a3a9b9 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/PlaybackRate.tsx @@ -0,0 +1,51 @@ +import { useAtom } from "jotai"; +import { Label, Slider } from "radix-ui"; + +import { DEFAULT_PLAYBACK_RATE } from "@/utils/constants"; +import { playbackRateAtom } from "@/state/playback"; + +const MIN_RATE = 100; +const MAX_RATE = 20_000; +const STEP = 100; + +function PlaybackRate() { + const [playbackRate, setPlaybackRate] = useAtom(playbackRateAtom); + + return ( +
+ + Playback Rate (Packets / Tick) + +
+ setPlaybackRate(value[0])} + className="relative flex h-4 flex-1 items-center" + > + + + + + + setPlaybackRate(Number(e.target.value))} + min={MIN_RATE} + max={MAX_RATE} + step={STEP} + /> +
+
+ ); +} + +export default PlaybackRate; diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx new file mode 100644 index 000000000000..2fd6b877c565 --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx @@ -0,0 +1,88 @@ +import { useAtomValue } from "jotai"; +import { Label, Separator } from "radix-ui"; + +import FuncSelect from "@/components/controls/visualizations/FuncSelect"; +import Histogram from "@/components/controls/visualizations/Histogram"; +import PlaybackRate from "@/components/controls/visualizations/PlaybackRate"; +import VisualizationSelect from "@/components/controls/visualizations/VisualizationsSelect"; +import { useTraceContext } from "@/hooks/trace"; +import { funcAtom } from "@/state/func"; +import { + type VisualizationMode, + visualizationModeAtom, +} from "@/state/visualization"; +import { FuncMeta } from "@/types"; + +const VISUALIZATION_MODE_TO_HISTOGRAM_DATA_KEY: Record< + VisualizationMode, + keyof FuncMeta +> = { + "True Values": "", + "Store Frequency": "store_count_histogram", + "Load Frequency": "load_count_histogram", + "Redundant Stores": "redundant_count_histogram", +}; + +const VISUALIZATION_MODE_TO_LABEL: Record = { + "True Values": "", + "Store Frequency": "Store Count", + "Load Frequency": "Load Count", + "Redundant Stores": "Redundant Store Count", +}; + +function VisualizationsPanel() { + const { funcs } = useTraceContext(); + const visualizationMode = useAtomValue(visualizationModeAtom); + const func = useAtomValue(funcAtom); + + return ( +
+
+ + Visualization + + +
+ {func && funcs[func] && visualizationMode !== "True Values" ? ( + <> + +
+ + Histogram + + <> + + ({ + x: stores, + y: pixels, + })) ?? [] + } + labels={{ x: VISUALIZATION_MODE_TO_LABEL[visualizationMode] }} + /> + +
+ + ) : null} + +
+ + Parameters + + +
+
+ ); +} + +export default VisualizationsPanel; diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx new file mode 100644 index 000000000000..2c0dee4fe3cf --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx @@ -0,0 +1,71 @@ +import { Select } from "radix-ui"; +import { useAtom } from "jotai"; + +import { + visualizationModeAtom, + type VisualizationMode, +} from "@/state/visualization"; + +const VISUALIZATION_MODES = [ + { value: "True Values", label: "True Values" }, + { value: "Store Frequency", label: "Store Frequency" }, + { value: "Load Frequency", label: "Load Frequency" }, + { value: "Redundant Stores", label: "Redundant Stores" }, +] as const; + +function VisualizationSelect() { + const [visualizationMode, setVisualizationMode] = useAtom( + visualizationModeAtom, + ); + + return ( + + setVisualizationMode(value as VisualizationMode) + } + > + + + + + + + + + + + {VISUALIZATION_MODES.map(({ value, label }) => ( + + {label} + + ))} + + + + ); +} + +export default VisualizationSelect; diff --git a/apps/halidoscope/frontend/src/components/shared/Canvas.tsx b/apps/halidoscope/frontend/src/components/shared/Canvas.tsx index d940481a5962..dcde7df6813f 100644 --- a/apps/halidoscope/frontend/src/components/shared/Canvas.tsx +++ b/apps/halidoscope/frontend/src/components/shared/Canvas.tsx @@ -6,12 +6,11 @@ import { type Node, type Edge, } from "@xyflow/react"; -import { useAtom } from "jotai"; +import { useSetAtom } from "jotai"; import * as React from "react"; import FuncCanvas from "@/components/views/tracer/FuncCanvas"; -import FuncEdge from "@/components/views/tracer/FuncEdge"; -import { FuncStats, NodeTypes } from "@/types"; +import { FuncMeta, NodeTypes } from "@/types"; import { buildEdges, buildNodes, getLayoutedElements } from "@/utils/graph"; import { funcAtom } from "@/state/func"; @@ -19,10 +18,6 @@ const NODE_TYPES = { funcCanvas: FuncCanvas, }; -const EDGE_TYPES = { - funcEdge: FuncEdge, -}; - function hideEdge(hidden: boolean) { return function handleVisibilityChange(edge: Edge) { return { @@ -33,7 +28,7 @@ function hideEdge(hidden: boolean) { } interface CanvasProps { - funcs: Record; + funcs: Record; dagEdges: Record; type: NodeTypes; } @@ -44,10 +39,10 @@ function Canvas({ funcs, dagEdges, type }: CanvasProps) { }, [funcs, dagEdges, type]); const [nodes, _setNodes, onNodesChange] = - useNodesState>(initialNodes); + useNodesState>(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [hidden, _setHidden] = React.useState(false); - const [_func, setFunc] = useAtom(funcAtom); + const setFunc = useSetAtom(funcAtom); const { zoom } = useViewport(); @@ -60,12 +55,11 @@ function Canvas({ funcs, dagEdges, type }: CanvasProps) { }, [hidden, setEdges]); return ( -
+
setFunc(node.data.name)} /> -
+
Zoom: {Math.round(zoom * 100)}%
diff --git a/apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx b/apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx index 8de719581946..ee3c9c8b39fc 100644 --- a/apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx +++ b/apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx @@ -1,12 +1,16 @@ -function HandleCircle({ zoom }: { zoom: number }) { +import { useViewport } from "@xyflow/react"; + +function HandleCircle() { + const { zoom } = useViewport(); + return ( diff --git a/apps/halidoscope/frontend/src/components/views/ViewTabs.tsx b/apps/halidoscope/frontend/src/components/views/ViewTabs.tsx deleted file mode 100644 index 186692ab9524..000000000000 --- a/apps/halidoscope/frontend/src/components/views/ViewTabs.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Tabs } from "radix-ui"; - -import Tracer from "@/components/views/tracer/Tracer"; - -function ViewTabs() { - return ( - - - - Tracer - - - - - - - ); -} - -export default ViewTabs; diff --git a/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx b/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx index 91cd6e942c3f..989290418c56 100644 --- a/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx +++ b/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx @@ -1,246 +1,148 @@ import { + getIncomers, + getOutgoers, Handle, Position, type Node, type NodeProps, - useViewport, + useEdges, + useNodes, } from "@xyflow/react"; -import * as d3 from "d3"; -import { useAtom } from "jotai"; +import { useAtomValue } from "jotai"; import * as React from "react"; import HandleCircle from "@/components/shared/HandleCircle"; -import { useCanvasRegistry } from "@/hooks/canvas-registry"; -import type { FuncStats, ChannelData } from "@/types"; -import { useTraceContext } from "@/hooks/trace"; -import { playbackModeAtom } from "@/state/playback"; +import type { FuncMeta } from "@/types"; +import { packetAtom } from "@/state/packet"; +import { visualizationModeAtom } from "@/state/visualization"; +import { renderAt, renderHeatmap, renderRedundant } from "@/utils/api"; -type FuncNode = Node; +type FuncNode = Node; -function FuncCanvas({ - id, - data: { name, width, height }, -}: NodeProps) { +function FuncCanvas({ data: { name, width, height } }: NodeProps) { const canvasRef = React.useRef(null); - const canvasRegistry = useCanvasRegistry(); - const [playbackMode] = useAtom(playbackModeAtom); - const { globalMaxStoreCount, globalMaxLoadCount } = useTraceContext(); - const { zoom } = useViewport(); - - const storeScale = React.useMemo( - () => - d3.scaleSequential(d3.interpolateReds).domain([0, globalMaxStoreCount]), - [globalMaxStoreCount], - ); - - const loadScale = React.useMemo( - () => - d3.scaleSequential(d3.interpolateBlues).domain([0, globalMaxLoadCount]), - [globalMaxLoadCount], + const globalIndex = useAtomValue(packetAtom); + const visualizationMode = useAtomValue(visualizationModeAtom); + + const nodes = useNodes(); + const edges = useEdges(); + const incomingEdgeCount = React.useMemo( + () => getIncomers({ id: name }, nodes, edges).length, + [name, nodes, edges], ); - - const applyChannel = React.useCallback( - (imageData: ImageDataArray, ch: ChannelData, offset: number) => { - for (let i = 0; i < ch.xs.length; i++) { - imageData[4 * (ch.ys[i] * width + ch.xs[i]) + offset] = ch.values[i]; - } - }, - [width], + const outgoingEdgeCount = React.useMemo( + () => getOutgoers({ id: name }, nodes, edges).length, + [name, nodes, edges], ); - const drawNormal = React.useCallback( - ({ - xs, - ys, - values, - r, - g, - b, - image, - ctx, - }: { - xs?: number[]; - ys?: number[]; - values?: number[]; - r?: ChannelData; - g?: ChannelData; - b?: ChannelData; - image: ImageData; - ctx: CanvasRenderingContext2D; - }) => { - const data = image.data; - - if (r) { - applyChannel(data, r, 0); - } - - if (g) { - applyChannel(data, g, 1); - } - - if (b) { - applyChannel(data, b, 2); - } - - if (xs && ys && values) { - for (let i = 0; i < xs.length; i++) { - const idx = 4 * (ys[i] * width + xs[i]); - const v = values[i]; - data[idx] = v; - data[idx + 1] = v; - data[idx + 2] = v; - data[idx + 3] = 255; + // Latest playhead position requested, and whether a render loop is draining. + // Together these coalesce rapid scrub updates: while a frame is in flight, + // newer indices just overwrite `latestIndexRef`, and the loop renders only + // the most recent one rather than every intermediate position. + const latestIndexRef = React.useRef(globalIndex); + const renderingRef = React.useRef(false); + + const paint = React.useCallback(async () => { + if (renderingRef.current) { + return; + } + + renderingRef.current = true; + try { + // Drain to the latest requested index, skipping any that arrived while a + // previous frame was rendering. + while (true) { + const target = latestIndexRef.current; + const buffer = await renderAt(name, target); + + const ctx = canvasRef.current?.getContext("2d"); + + if (ctx) { + const pixels = new Uint8ClampedArray(buffer); + ctx.putImageData(new ImageData(pixels, width, height), 0, 0); } - } - - ctx.putImageData(image, 0, 0); - }, - [applyChannel, width], - ); - - const drawStoreCounts = React.useCallback( - ({ - xs, - ys, - counts, - storeCountBuf, - image, - ctx, - }: { - xs?: number[]; - ys?: number[]; - counts?: number[]; - storeCountBuf: Int32Array; - image: ImageData; - ctx: CanvasRenderingContext2D; - }) => { - if (!xs || !ys) return; - - const data = image.data; - for (let i = 0; i < xs.length; i++) { - const idx = ys[i] * width + xs[i]; - storeCountBuf[idx] += counts ? counts[i] : 1; - const { r, g, b } = d3.color(storeScale(storeCountBuf[idx]))!.rgb(); - data[4 * idx] = r; - data[4 * idx + 1] = g; - data[4 * idx + 2] = b; - data[4 * idx + 3] = 255; - } - - ctx.putImageData(image, 0, 0); - }, - [storeScale, width], - ); - - const drawLoadCounts = React.useCallback( - ({ - xs, - ys, - counts, - loadCountBuf, - image, - ctx, - }: { - xs?: number[]; - ys?: number[]; - counts?: number[]; - loadCountBuf: Int32Array; - image: ImageData; - ctx: CanvasRenderingContext2D; - }) => { - if (!xs || !ys) return; - - const data = image.data; - - for (let i = 0; i < xs.length; i++) { - const idx = ys[i] * width + xs[i]; - loadCountBuf[idx] += counts ? counts[i] : 1; - const { r, g, b } = d3.color(loadScale(loadCountBuf[idx]))!.rgb(); - data[4 * idx] = r; - data[4 * idx + 1] = g; - data[4 * idx + 2] = b; - data[4 * idx + 3] = 255; + if (latestIndexRef.current === target) { + break; + } } - - ctx.putImageData(image, 0, 0); - }, - [loadScale, width], - ); + } catch (err) { + console.error(`Failed to render ${name}:`, err); + } finally { + renderingRef.current = false; + } + }, [name, width, height]); React.useEffect(() => { - const ctx = canvasRef.current?.getContext("2d"); - if (!ctx) return; + if (visualizationMode !== "True Values") return; + latestIndexRef.current = globalIndex; + paint(); + }, [globalIndex, visualizationMode, paint]); - const image = ctx.createImageData(width, height); - const storeCountBuf = new Int32Array(width * height); - const loadCountBuf = new Int32Array(width * height); - - const reset = () => { - const data = image.data; - - data.fill(0); - // Opaque black background. - for (let i = 3; i < data.length; i += 4) data[i] = 255; - ctx.putImageData(image, 0, 0); - - // Reset the store/load count buffers. - storeCountBuf.fill(0); - loadCountBuf.fill(0); - }; - - reset(); - - const unregister = canvasRegistry.register(id, { - draw: ({ xs, ys, values, counts, r, g, b }) => { - switch (playbackMode) { - case "normal": - drawNormal({ xs, ys, values, r, g, b, image, ctx }); - break; - case "stores": - drawStoreCounts({ xs, ys, counts, storeCountBuf, image, ctx }); - break; - case "loads": - drawLoadCounts({ xs, ys, counts, loadCountBuf, image, ctx }); - break; + React.useEffect(() => { + if ( + visualizationMode !== "Store Frequency" && + visualizationMode !== "Load Frequency" + ) + return; + renderHeatmap(name, globalIndex, visualizationMode) + .then((buffer) => { + const ctx = canvasRef.current?.getContext("2d"); + if (ctx) { + ctx.putImageData( + new ImageData(new Uint8ClampedArray(buffer), width, height), + 0, + 0, + ); } - }, - clear: reset, - }); + }) + .catch((err) => + console.error(`Failed to render heatmap for ${name}:`, err), + ); + }, [globalIndex, visualizationMode, name, width, height]); - return unregister; - }, [ - id, - width, - height, - canvasRegistry, - applyChannel, - drawNormal, - drawStoreCounts, - drawLoadCounts, - playbackMode, - ]); + React.useEffect(() => { + if (visualizationMode !== "Redundant Stores") return; + renderRedundant(name, globalIndex) + .then((buffer) => { + const ctx = canvasRef.current?.getContext("2d"); + if (ctx) { + ctx.putImageData( + new ImageData(new Uint8ClampedArray(buffer), width, height), + 0, + 0, + ); + } + }) + .catch((err) => + console.error(`Failed to render redundant for ${name}:`, err), + ); + }, [globalIndex, visualizationMode, name, width, height]); return (
- + {name} - - - - - - + {incomingEdgeCount > 0 ? ( + + + + ) : null} + {outgoingEdgeCount > 0 ? ( + + + + ) : null}
); } diff --git a/apps/halidoscope/frontend/src/components/views/tracer/FuncEdge.tsx b/apps/halidoscope/frontend/src/components/views/tracer/FuncEdge.tsx deleted file mode 100644 index bada3120c706..000000000000 --- a/apps/halidoscope/frontend/src/components/views/tracer/FuncEdge.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseEdge, getStraightPath, type EdgeProps } from "@xyflow/react"; - -function FuncEdge({ id, sourceX, sourceY, targetX, targetY }: EdgeProps) { - const [edgePath] = getStraightPath({ - sourceX, - sourceY, - targetX, - targetY, - }); - - return ( - <> - - - ); -} - -export default FuncEdge; diff --git a/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx b/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx index 07fc33ac3bcd..914bdf5307c5 100644 --- a/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx +++ b/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx @@ -2,40 +2,30 @@ import { ReactFlowProvider } from "@xyflow/react"; import Canvas from "@/components/shared/Canvas"; import Timeline from "@/components/views/tracer/TracerTimeline"; -import { CanvasRegistryProvider } from "@/hooks/canvas-registry"; import { useTraceContext } from "@/hooks/trace"; import ControlTabs from "@/components/controls/ControlTabs"; function Tracer() { - const { sessionId, funcs, dagEdges, packetCount, canvasRegistry } = - useTraceContext(); + const { funcs, dagEdges, packetCount } = useTraceContext(); return ( - -
-
-
- {Object.keys(funcs).length > 0 ? ( - <> - - - - - - ) : ( -
-

Loading trace...

-
- )} +
+
+ {Object.keys(funcs).length > 0 ? ( + <> + + + + + + ) : ( +
+

Loading trace...

- -
+ )}
- + +
); } diff --git a/apps/halidoscope/frontend/src/components/views/tracer/TracerTimeline.tsx b/apps/halidoscope/frontend/src/components/views/tracer/TracerTimeline.tsx index 268bb51d6b24..d8946197643d 100644 --- a/apps/halidoscope/frontend/src/components/views/tracer/TracerTimeline.tsx +++ b/apps/halidoscope/frontend/src/components/views/tracer/TracerTimeline.tsx @@ -1,96 +1,38 @@ import * as d3 from "d3"; -import { useAtom } from "jotai"; +import { useAtom, useSetAtom } from "jotai"; import { Slider } from "radix-ui"; import * as React from "react"; -import { CanvasRegistry } from "@/hooks/canvas-registry"; -import { playbackModeAtom } from "@/state/playback"; -import { RangeRequest, RenderResponse } from "@/types"; -import { - PLAYBACK_INTERVAL_MS, - PLAYBACK_STEP, - SCRUB_DEBOUNCE_MS, - WS_ENDPOINT, -} from "@/utils/constants"; +import { packetAtom } from "@/state/packet"; +import { playbackRateAtom } from "@/state/playback"; +import { SCRUB_DEBOUNCE_MS } from "@/utils/constants"; interface TracerTimelineProps { packetCount: number; - sessionId: string; - canvasRegistry: CanvasRegistry | null; } -function TracerTimeline({ - packetCount, - sessionId, - canvasRegistry, -}: TracerTimelineProps) { - // Track the current packet index. +function TracerTimeline({ packetCount }: TracerTimelineProps) { + // Local slider position, for a smooth thumb independent of render cadence. const [packetIndex, setPacketIndex] = React.useState(0); const [playing, setPlaying] = React.useState(false); - const [playbackMode] = useAtom(playbackModeAtom); - const wsRef = React.useRef(null); + const [playbackRate] = useAtom(playbackRateAtom); + const setGlobalIndex = useSetAtom(packetAtom); - // Use refs to synchronously track mutable state without triggering re-renders. - const inFlightRef = React.useRef(false); - const timeRef = React.useRef(0); - const renderedEndRef = React.useRef(0); - const pendingEndRef = React.useRef(0); + // Mirror the latest index synchronously for the playback interval closure. + const indexRef = React.useRef(0); const scrubTimerRef = React.useRef(null); - // Send the next range needed to bring the canvases to the current playhead. - // Only one request is in flight at a time; on each response we pump again so - // the canvases catch up to wherever the playhead has moved. - const pump = React.useCallback(() => { - if ( - inFlightRef.current || - !canvasRegistry || - !wsRef.current || - wsRef.current.readyState !== WebSocket.OPEN - ) { - console.warn("Skipping pump: ", { - inFlight: inFlightRef.current, - hasRegistry: !!canvasRegistry, - wsRef: wsRef.current, - readyState: wsRef.current?.readyState, - }); - return; - } - - const targetEnd = timeRef.current + 1; - const rendered = renderedEndRef.current; - - if (targetEnd === rendered) { - return; - } - - let start: number; - if (targetEnd < rendered) { - // Backward: discard accumulated pixels and re-render from the start. - canvasRegistry.clearAll(); - renderedEndRef.current = 0; - start = 0; - } else { - // Forward: render only the new delta on top of the existing buffers. - start = rendered; - } - - // Send the request over the WebSocket. - inFlightRef.current = true; - pendingEndRef.current = targetEnd; - const request: RangeRequest = { - start, - end: targetEnd, - }; - - wsRef.current.send(JSON.stringify(request)); - }, [canvasRegistry]); + const commitIndex = React.useCallback((next: number) => { + indexRef.current = next; + setPacketIndex(next); + }, []); - // Scrub: track the playhead immediately, but defer rendering until the slider - // settles so a drag doesn't fire a request per intermediate position. + // Scrub: move the thumb immediately, but defer the (debounced) render so a + // drag doesn't fire a request per intermediate position. FuncCanvases + // coalesce in flight, but debouncing still trims redundant render cycles. const onScrub = React.useCallback( (next: number) => { - timeRef.current = next; - setPacketIndex(next); + commitIndex(next); if (scrubTimerRef.current !== null) { window.clearTimeout(scrubTimerRef.current); @@ -98,100 +40,52 @@ function TracerTimeline({ scrubTimerRef.current = window.setTimeout(() => { scrubTimerRef.current = null; - pump(); + setGlobalIndex(next); }, SCRUB_DEBOUNCE_MS); }, - [pump], + [commitIndex, setGlobalIndex], ); const onTogglePlay = React.useCallback(() => { // Starting from the end replays from the beginning. - if (!playing && timeRef.current >= packetCount - 1) { - timeRef.current = 0; - setPacketIndex(0); + if (!playing && indexRef.current >= packetCount - 1) { + commitIndex(0); + setGlobalIndex(0); } setPlaying((p) => !p); - }, [playing, packetCount]); + }, [playing, packetCount, commitIndex, setGlobalIndex]); + // Playback loop: advance the playhead on a fixed interval and push each step + // straight to the global index. Canvases coalesce if rendering lags. React.useEffect(() => { - if (!sessionId || !canvasRegistry) { + if (!playing) { return; } - let wsPath; - switch (playbackMode) { - case "loads": - wsPath = `${WS_ENDPOINT}/ws/${sessionId}/loads`; - break; - case "stores": - wsPath = `${WS_ENDPOINT}/ws/${sessionId}/stores`; - break; - default: - wsPath = `${WS_ENDPOINT}/ws/${sessionId}`; - break; - } - wsRef.current = new WebSocket(wsPath); + let animationFrameId: number | null = null; - wsRef.current.onopen = () => { - pump(); - }; + function step() { + const next = Math.min(indexRef.current + playbackRate, packetCount - 1); + commitIndex(next); + setGlobalIndex(next); - wsRef.current.onmessage = (event: MessageEvent) => { - const res: RenderResponse = JSON.parse(event.data); - if (canvasRegistry) { - for (const update of res.updates) { - canvasRegistry.dispatch(update); - } + if (next >= packetCount - 1 && animationFrameId !== null) { + setPlaying(false); + cancelAnimationFrame(animationFrameId); } - if (res.done) { - renderedEndRef.current = pendingEndRef.current; - inFlightRef.current = false; - pump(); - } - }; + animationFrameId = requestAnimationFrame(step); + } - wsRef.current.onerror = (err: Event) => - console.error("WebSocket error:", err); - wsRef.current.onclose = () => {}; + step(); return () => { - // Reset inFlight so a torn-down socket can't leave the next connection - // permanently blocked on the guard in pump. - inFlightRef.current = false; - if (wsRef.current) { - wsRef.current.close(); - wsRef.current.onopen = null; - wsRef.current.onmessage = null; - wsRef.current.onerror = null; - wsRef.current.onclose = null; - wsRef.current = null; + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId); } }; - }, [pump, canvasRegistry, sessionId, playbackMode]); - - // Playback loop: advance the playhead on a fixed interval and pump after each - // step. Rendering may lag the playhead on large traces; it catches up via the - // pump-on-response in the WebSocket handler. - React.useEffect(() => { - if (!playing) { - return; - } - - const id = window.setInterval(() => { - const next = Math.min(timeRef.current + PLAYBACK_STEP, packetCount - 1); - timeRef.current = next; - setPacketIndex(next); - pump(); - - if (next >= packetCount - 1) { - setPlaying(false); - } - }, PLAYBACK_INTERVAL_MS); - - return () => window.clearInterval(id); - }, [packetCount, playing, pump]); + }, [packetCount, playing, commitIndex, setGlobalIndex, playbackRate]); const disabled = packetCount <= 0; const ticks = d3 @@ -199,12 +93,12 @@ function TracerTimeline({ .filter((t) => t > 0 && t < packetCount - 1); return ( -
-
+
+
-
+
{ticks.map((tick) => (
-

+

{d3.format(".2s")(tick)}

))} onScrub(values[0])} value={[packetIndex]} disabled={disabled} > - - + +
-
+
Packets {packetIndex.toLocaleString()} /{" "} diff --git a/apps/halidoscope/frontend/src/hooks/canvas-registry.ts b/apps/halidoscope/frontend/src/hooks/canvas-registry.ts deleted file mode 100644 index 8fbc8fc6e7a4..000000000000 --- a/apps/halidoscope/frontend/src/hooks/canvas-registry.ts +++ /dev/null @@ -1,93 +0,0 @@ -import * as React from "react"; - -import { FuncUpdate } from "../types"; - -/** - * The imperative draw surface a {@link FuncCanvas} exposes to the bus. The - * canvas owns its persistent pixel buffer so that store writes accumulate - * across incremental range updates. - */ -export interface CanvasHandle { - /** Apply a range's pixel writes on top of the existing buffer. */ - draw: (update: FuncUpdate) => void; - /** Reset the buffer to empty (used on backward scrubs / new traces). */ - clear: () => void; -} - -/** - * Routes backend {@link FuncUpdate}s to the matching {@link FuncCanvas} without - * pushing pixel data through React state. Canvases register a {@link CanvasHandle} - * keyed by their qualified func name (the node id); the registry resolves a - * packet's raw func name to that key using the same matching the backend uses. - */ -export class CanvasRegistry { - private handlers = new Map(); - // raw packet func name -> resolved qualified handler key (or null if none). - private resolveCache = new Map(); - - /** Register a canvas under its qualified func name; returns an unregister fn. */ - register(qualifiedName: string, handle: CanvasHandle): () => void { - this.handlers.set(qualifiedName, handle); - this.resolveCache.clear(); - - return () => { - if (this.handlers.get(qualifiedName) === handle) { - this.handlers.delete(qualifiedName); - this.resolveCache.clear(); - } - }; - } - - /** - * Resolve a raw func name (e.g. "f0") to a registered qualified key (e.g. - * "local_laplacian:f0"). Mirrors the backend's `_get_func_item_for_packet`: - * exact match, then a "pipeline:func" suffix, then a substring fallback. - */ - private resolve(raw: string): string | null { - const cached = this.resolveCache.get(raw); - if (cached !== undefined) return cached; - - let match: string | null = null; - if (this.handlers.has(raw)) { - match = raw; - } else { - for (const name of this.handlers.keys()) { - if (name.endsWith(`:${raw}`) || name.includes(raw)) { - match = name; - break; - } - } - } - - this.resolveCache.set(raw, match); - - return match; - } - - /** Route one func's updates to its canvas, if a matching one is registered. */ - dispatch(update: FuncUpdate): void { - const key = this.resolve(update.func); - if (key) this.handlers.get(key)?.draw(update); - } - - /** Clear every registered canvas (backward scrub / re-render from scratch). */ - clearAll(): void { - for (const handle of this.handlers.values()) handle.clear(); - } -} - -const CanvasRegistryContext = React.createContext(null); -export const CanvasRegistryProvider = CanvasRegistryContext.Provider; - -/** Access the {@link CanvasRegistry} provided by an ancestor {@link CanvasRegistryProvider}. */ -export function useCanvasRegistry(): CanvasRegistry { - const bus = React.useContext(CanvasRegistryContext); - - if (!bus) { - throw new Error( - "useCanvasRegistry must be used within a CanvasRegistryProvider", - ); - } - - return bus; -} diff --git a/apps/halidoscope/frontend/src/hooks/trace.ts b/apps/halidoscope/frontend/src/hooks/trace.ts index 9173194c616c..b6137cedbcc4 100644 --- a/apps/halidoscope/frontend/src/hooks/trace.ts +++ b/apps/halidoscope/frontend/src/hooks/trace.ts @@ -1,24 +1,21 @@ import * as React from "react"; -import { FuncStats } from "@/types"; -import { CanvasRegistry } from "@/hooks/canvas-registry"; +import { FuncMeta } from "@/types"; const TraceContext = React.createContext<{ - sessionId: string; - funcs: Record; + funcs: Record; dagEdges: Record; packetCount: number; - canvasRegistry: CanvasRegistry | null; globalMaxStoreCount: number; globalMaxLoadCount: number; + globalMaxRedundantCount: number; }>({ - sessionId: "", funcs: {}, dagEdges: {}, packetCount: 0, - canvasRegistry: null, globalMaxStoreCount: 0, globalMaxLoadCount: 0, + globalMaxRedundantCount: 0, }); export const TraceContextProvider = TraceContext.Provider; diff --git a/apps/halidoscope/frontend/src/state/func.ts b/apps/halidoscope/frontend/src/state/func.ts index 87a03d02956b..21b50843c1d9 100644 --- a/apps/halidoscope/frontend/src/state/func.ts +++ b/apps/halidoscope/frontend/src/state/func.ts @@ -1,3 +1,3 @@ import { atom } from "jotai"; -export const funcAtom = atom(null); +export const funcAtom = atom(""); diff --git a/apps/halidoscope/frontend/src/state/packet.ts b/apps/halidoscope/frontend/src/state/packet.ts new file mode 100644 index 000000000000..765e8fcaca2a --- /dev/null +++ b/apps/halidoscope/frontend/src/state/packet.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const packetAtom = atom(0); diff --git a/apps/halidoscope/frontend/src/state/playback.ts b/apps/halidoscope/frontend/src/state/playback.ts index 5eeeaf6735a5..96d304bc049d 100644 --- a/apps/halidoscope/frontend/src/state/playback.ts +++ b/apps/halidoscope/frontend/src/state/playback.ts @@ -1,5 +1,5 @@ import { atom } from "jotai"; -export type PlaybackMode = "normal" | "stores" | "loads"; +import { DEFAULT_PLAYBACK_RATE } from "@/utils/constants"; -export const playbackModeAtom = atom("normal"); +export const playbackRateAtom = atom(DEFAULT_PLAYBACK_RATE); diff --git a/apps/halidoscope/frontend/src/state/visualization.ts b/apps/halidoscope/frontend/src/state/visualization.ts new file mode 100644 index 000000000000..f8907226a48f --- /dev/null +++ b/apps/halidoscope/frontend/src/state/visualization.ts @@ -0,0 +1,9 @@ +import { atom } from "jotai"; + +export type VisualizationMode = + | "True Values" + | "Store Frequency" + | "Load Frequency" + | "Redundant Stores"; + +export const visualizationModeAtom = atom("True Values"); diff --git a/apps/halidoscope/frontend/src/types/index.ts b/apps/halidoscope/frontend/src/types/index.ts index abe1d879f600..ba8bff065b4e 100644 --- a/apps/halidoscope/frontend/src/types/index.ts +++ b/apps/halidoscope/frontend/src/types/index.ts @@ -1,60 +1,44 @@ -export interface FuncStats extends Record { +/** How a Func's values are mapped to pixels. Mirrors the Rust `RenderMode`. */ +export type RenderMode = "grayscale" | "rgb"; + +/** + * Per-Func metadata returned by the `open_trace` command. Mirrors the Rust + * `FuncMeta`. Carries everything the UI needs to size canvases, bound the + * scrub timeline, and populate the inspector panel. + */ +export interface FuncMeta extends Record { name: string; width: number; height: number; + channels: number; + default_mode: RenderMode; + num_stores: number; min_coords: number[]; max_coords: number[]; - min_value: number; - max_value: number; + min_value: number | null; + max_value: number | null; max_store_count: number; max_load_count: number; + max_redundant_count: number; + /** + * Frequency distributions of per-pixel counts, indexed by count value (index `k` holds the + * number of pixel locations with exactly `k` stores/loads/redundant stores). The `0` bin is + * included. Length is the corresponding `max_*_count + 1`; empty when the Func has no usable + * extent. Ready to render directly as a histogram. + */ + store_count_histogram: number[]; + load_count_histogram: number[]; + redundant_count_histogram: number[]; } -/** Per-channel pixel writes for one color channel of a color func. */ -export interface ChannelData { - xs: number[]; - ys: number[]; - values: number[]; -} - -/** - * A single Func's pixel updates for a rendered range, as returned by the - * backend. - * - * @property func The name of the Func being updated. - * @property xs The x-coordinates of the updated pixels. - * @property ys The y-coordinates of the updated pixels. - * @property values Normalized 0-255 values for the updated pixels (Grayscale). - * @property counts Incremental store counts at each (x, y) for the stores endpoint. - * @property r The red channel for the updated pixels. - * @property g The green channel for the updated pixels. - * @property b The blue channel for the updated pixels. - */ -export interface FuncUpdate { - func: string; - xs?: number[]; - ys?: number[]; - values?: number[]; - counts?: number[]; - r?: ChannelData; - g?: ChannelData; - b?: ChannelData; -} - -/** - * A range request sent to the backend WebSocket. Renders packets in [start, end). - */ -export interface RangeRequest { - start: number; - end: number; -} - -/** The backend's response to a {@link RangeRequest}. */ -export interface RenderResponse { - updates: FuncUpdate[]; - done: boolean; - start: number; - end: number; +/** Top-level payload returned by `open_trace`. Mirrors the Rust `TraceMeta`. */ +export interface TraceMeta { + funcs: FuncMeta[]; + total_packets: number; + dag_edges: Record; + global_max_store_count: number; + global_max_load_count: number; + global_max_redundant_count: number; } export type NodeTypes = "funcCanvas"; diff --git a/apps/halidoscope/frontend/src/utils/api.ts b/apps/halidoscope/frontend/src/utils/api.ts index 23f0680db90c..d3d8590a3a58 100644 --- a/apps/halidoscope/frontend/src/utils/api.ts +++ b/apps/halidoscope/frontend/src/utils/api.ts @@ -1,63 +1,62 @@ -import { BACKEND_ENDPOINT } from "./constants"; +import { invoke } from "@tauri-apps/api/core"; -import type { FuncStats } from "../types"; - -interface LoadTraceResponse { - session_id: string; - funcs: Record; - dag_edges: Record; - num_packets: number; - global_max_store_count: number; - global_max_load_count: number; -} +import type { RenderMode, TraceMeta } from "../types"; +import type { VisualizationMode } from "../state/visualization"; /** - * Load a Halide trace from the specified path on disk. + * Parse a Halide trace from disk and return its metadata. * - * @param path The absolute path to the trace file on disk. Passed to the - * --trace CLI argument. If a relative path is provided, the calling code is - * responsible for resolving it against the current working directory. - * @param signal An {@link AbortSignal} to cancel the request. - * @returns An {@link LoadTraceResponse} containing the session ID, func stats, - * DAG edges, and total number of packets in the trace. - * @throws An error if the request fails or the response is not OK. + * @param path Absolute path to the `.hltrace` file. + * @returns The {@link TraceMeta} describing every Func and the global timeline. + * @throws If the backend fails to read or parse the trace. */ -export async function loadTracePath( - path: string, - signal: AbortSignal, -): Promise { - const response = await fetch(`${BACKEND_ENDPOINT}/load-path`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path }), - signal, - }); - - if (!response.ok) { - throw new Error(`Failed to load trace: ${response.statusText}`); - } - - return response.json(); +export async function openTrace(path: string): Promise { + return invoke("open_trace", { path }); } /** - * Deregister a trace session on the backend, freeing associated resources. + * Render a Func's framebuffer state at a point on the global timeline. + * + * The backend accumulates the Func's stores up to `globalIndex` and returns a + * `width * height * 4` RGBA8 buffer (delivered as an `ArrayBuffer`), ready to + * hand to `putImageData`. * - * @param session The session ID to deregister. - * @returns A promise resolving to the JSON response from the backend. - * @throws An error if the request fails or the response is not OK. + * @param func The qualified Func name. + * @param globalIndex Position on the global packet timeline. + * @param mode Optional override of the Func's inferred render mode. + * @returns The raw RGBA8 bytes for the frame. */ -export async function deregisterTrace( - session: string, -): Promise<{ deleted: string }> { - const response = await fetch(`${BACKEND_ENDPOINT}/session/${session}`, { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - }); +export async function renderAt( + func: string, + globalIndex: number, + mode?: RenderMode, +): Promise { + return invoke("render_at", { func, globalIndex, mode }); +} - if (!response.ok) { - throw new Error(`Failed to deregister trace: ${response.statusText}`); - } +/** + * Render a heatmap of store or load counts for `func` up to `globalIndex`. + * The mode must be "Store Frequency" or "Load Frequency" — the string is + * passed directly to the Rust backend. Returns a `width * height * 4` RGBA8 + * buffer with the inferno colormap applied. + */ +export async function renderHeatmap( + func: string, + globalIndex: number, + mode: Exclude, +): Promise { + return invoke("render_heatmap", { func, globalIndex, mode }); +} - return response.json(); +/** + * Render a heatmap of redundant store counts for `func` up to `globalIndex`. + * A store is redundant when it writes the same value to a location that already + * holds that value. Returns a `width * height * 4` RGBA8 buffer with the Reds + * colormap applied; pixels with zero redundant stores are black. + */ +export async function renderRedundant( + func: string, + globalIndex: number, +): Promise { + return invoke("render_redundant", { func, globalIndex }); } diff --git a/apps/halidoscope/frontend/src/utils/constants.ts b/apps/halidoscope/frontend/src/utils/constants.ts index 23a2b12e10e3..96a58584a69e 100644 --- a/apps/halidoscope/frontend/src/utils/constants.ts +++ b/apps/halidoscope/frontend/src/utils/constants.ts @@ -1,9 +1,3 @@ -export const BACKEND_ENDPOINT = "http://localhost:8765"; -export const WS_ENDPOINT = "ws://localhost:8765"; - -/** Packets advanced per playback tick (mirrors neotrace's playback step). */ -export const PLAYBACK_STEP = 10000; -/** Playback tick interval in ms (~33fps, mirrors neotrace). */ -export const PLAYBACK_INTERVAL_MS = 30; +export const DEFAULT_PLAYBACK_RATE = 10000; /** Debounce window in ms before a settled scrub position is rendered. */ export const SCRUB_DEBOUNCE_MS = 50; diff --git a/apps/halidoscope/frontend/src/utils/graph.ts b/apps/halidoscope/frontend/src/utils/graph.ts index e6fe770f78a6..f30f3e4f3b18 100644 --- a/apps/halidoscope/frontend/src/utils/graph.ts +++ b/apps/halidoscope/frontend/src/utils/graph.ts @@ -1,7 +1,7 @@ import Dagre from "@dagrejs/dagre"; import type { Node, Edge } from "@xyflow/react"; -import { FuncStats, NodeTypes } from "../types"; +import { FuncMeta, NodeTypes } from "../types"; /** * Build xyflow nodes from the backend's funcs payload, which maps Halide func @@ -9,9 +9,9 @@ import { FuncStats, NodeTypes } from "../types"; * @returns */ export function buildNodes( - funcs: Record, + funcs: Record, type: NodeTypes, -): Node[] { +): Node[] { return Object.entries(funcs).map(([name, stats]) => { return { id: name, @@ -37,7 +37,11 @@ export function buildNodes( * @returns An array of edges formatted for use with xyflow, where each edge has an id, source, and target. */ export function buildEdges(dagEdges: Record): Edge[] { - const edges: { id: string; source: string; target: string }[] = []; + const edges: { + id: string; + source: string; + target: string; + }[] = []; for (const [producer, consumers] of Object.entries(dagEdges)) { for (const consumer of consumers) { @@ -53,9 +57,9 @@ export function buildEdges(dagEdges: Record): Edge[] { } export function getLayoutedElements( - nodes: Node[], + nodes: Node[], edges: Edge[], -): { nodes: Node[]; edges: Edge[] } { +): { nodes: Node[]; edges: Edge[] } { const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: 80 }); From 665ef4ba093320892b29b1553836db63e4e0612b Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Wed, 24 Jun 2026 12:17:08 -0700 Subject: [PATCH 12/67] feat: Add support for Reuse Distance visualization and highlight live Funcs on playback. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/frontend/package.json | 1 + apps/halidoscope/frontend/pnpm-lock.yaml | 9 + .../frontend/src-tauri/src/commands.rs | 64 ++- .../halidoscope/frontend/src-tauri/src/lib.rs | 3 +- .../frontend/src-tauri/src/render.rs | 269 ++++++++++++- .../frontend/src-tauri/src/trace.rs | 375 ++++++++++++++---- apps/halidoscope/frontend/src/App.css | 4 +- apps/halidoscope/frontend/src/App.tsx | 5 + .../controls/visualizations/FuncSelect.tsx | 62 --- .../controls/visualizations/GraphDisplay.tsx | 56 +++ .../controls/visualizations/Histogram.tsx | 118 +++--- .../visualizations/HistogramSelect.tsx | 124 ++++++ .../visualizations/VisualizationsPanel.tsx | 100 ++++- .../visualizations/VisualizationsSelect.tsx | 1 + .../frontend/src/components/shared/Canvas.tsx | 35 +- .../src/components/shared/HandleCircle.tsx | 2 +- .../components/views/tracer/FuncCanvas.tsx | 153 +++---- .../src/components/views/tracer/Tracer.tsx | 2 +- apps/halidoscope/frontend/src/hooks/trace.ts | 2 + apps/halidoscope/frontend/src/state/graph.ts | 4 + .../frontend/src/state/histogram.ts | 5 + .../frontend/src/state/visualization.ts | 3 +- apps/halidoscope/frontend/src/types/index.ts | 11 +- apps/halidoscope/frontend/src/utils/api.ts | 16 +- 24 files changed, 1078 insertions(+), 346 deletions(-) delete mode 100644 apps/halidoscope/frontend/src/components/controls/visualizations/FuncSelect.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/visualizations/GraphDisplay.tsx create mode 100644 apps/halidoscope/frontend/src/components/controls/visualizations/HistogramSelect.tsx create mode 100644 apps/halidoscope/frontend/src/state/graph.ts create mode 100644 apps/halidoscope/frontend/src/state/histogram.ts diff --git a/apps/halidoscope/frontend/package.json b/apps/halidoscope/frontend/package.json index 5d6bed9435bc..41e655fc34e6 100644 --- a/apps/halidoscope/frontend/package.json +++ b/apps/halidoscope/frontend/package.json @@ -18,6 +18,7 @@ "@tauri-apps/plugin-cli": "^2.4.1", "@tauri-apps/plugin-opener": "^2", "@xyflow/react": "^12.11.0", + "clsx": "^2.1.1", "d3": "^7.9.0", "jotai": "^2.20.1", "radix-ui": "^1.4.3", diff --git a/apps/halidoscope/frontend/pnpm-lock.yaml b/apps/halidoscope/frontend/pnpm-lock.yaml index 8e6dd792ac24..0bc01a4b1dda 100644 --- a/apps/halidoscope/frontend/pnpm-lock.yaml +++ b/apps/halidoscope/frontend/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@xyflow/react': specifier: ^12.11.0 version: 12.11.0(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + clsx: + specifier: ^2.1.1 + version: 2.1.1 d3: specifier: ^7.9.0 version: 7.9.0 @@ -1514,6 +1517,10 @@ packages: classcat@5.0.5: resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -3824,6 +3831,8 @@ snapshots: classcat@5.0.5: {} + clsx@2.1.1: {} + commander@7.2.0: {} convert-source-map@2.0.0: {} diff --git a/apps/halidoscope/frontend/src-tauri/src/commands.rs b/apps/halidoscope/frontend/src-tauri/src/commands.rs index 2ab46f79a4a2..bc726caf67f6 100644 --- a/apps/halidoscope/frontend/src-tauri/src/commands.rs +++ b/apps/halidoscope/frontend/src-tauri/src/commands.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use tauri::ipc::Response; use tauri::State; -use crate::render::{HeatmapState, RedundantState, RenderState}; +use crate::render::{HeatmapState, RedundantState, RenderState, ReuseDistanceState}; use crate::trace::Trace; /// How a Func's values are mapped to pixels. @@ -49,10 +49,7 @@ pub struct FuncMeta { pub height: u32, pub channels: u32, pub default_mode: RenderMode, - /// Number of store events for this Func across the whole trace. pub num_stores: u32, - /// Per-dimension coordinate extent, half-open `[min, max)`. Surfaced for the - /// funcs inspector panel. pub min_coords: Vec, pub max_coords: Vec, pub min_value: Option, @@ -60,12 +57,13 @@ pub struct FuncMeta { pub max_store_count: i32, pub max_load_count: i32, pub max_redundant_count: i32, - /// Frequency distributions of per-pixel counts, indexed by count value (the `0` bin is - /// included). Length is the corresponding `max_*_count + 1`; empty when the Func has no - /// usable extent. Rendered directly as histograms by the frontend. + pub max_reuse_distance: i64, pub store_count_histogram: Vec, pub load_count_histogram: Vec, pub redundant_count_histogram: Vec, + pub reuse_distance_histogram: Vec, + pub liveness_start: u32, + pub liveness_end: u32, } /// Top-level payload returned by `open_trace`. @@ -77,6 +75,7 @@ pub struct TraceMeta { pub global_max_store_count: i32, pub global_max_load_count: i32, pub global_max_redundant_count: i32, + pub global_max_reuse_distance: i64, } impl TraceMeta { @@ -87,6 +86,7 @@ impl TraceMeta { let mut global_max_store_count = 0; let mut global_max_load_count = 0; let mut global_max_redundant_count = 0; + let mut global_max_reuse_distance = 0i64; let funcs = trace .funcs @@ -110,6 +110,11 @@ impl TraceMeta { if stats.max_redundant_count > global_max_redundant_count { global_max_redundant_count = stats.max_redundant_count; } + if stats.max_reuse_distance > global_max_reuse_distance { + global_max_reuse_distance = stats.max_reuse_distance; + } + + let liveness_range = trace.func_liveness_range(name).unwrap_or(&(0, 0)); FuncMeta { name: name.clone(), @@ -125,9 +130,13 @@ impl TraceMeta { max_store_count: stats.max_store_count, max_load_count: stats.max_load_count, max_redundant_count: stats.max_redundant_count, + max_reuse_distance: stats.max_reuse_distance, store_count_histogram: stats.store_count_histogram.clone(), load_count_histogram: stats.load_count_histogram.clone(), redundant_count_histogram: stats.redundant_count_histogram.clone(), + reuse_distance_histogram: stats.reuse_distance_histogram.clone(), + liveness_start: liveness_range.0, + liveness_end: liveness_range.1, } }) .collect(); @@ -145,6 +154,7 @@ impl TraceMeta { global_max_store_count, global_max_load_count, global_max_redundant_count, + global_max_reuse_distance, } } } @@ -158,6 +168,7 @@ struct Loaded { renderers: HashMap, heatmap_renderers: HashMap, redundant_renderers: HashMap, + reuse_distance_renderers: HashMap, } /// App-wide state managed by Tauri. A single trace is loaded at a time; opening a new one replaces @@ -182,6 +193,7 @@ pub fn open_trace(path: String, state: State) -> Result, +) -> Result { + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + let loaded = guard.as_mut().ok_or("no trace loaded")?; + let Loaded { + trace, + reuse_distance_renderers, + .. + } = loaded; + + if !reuse_distance_renderers.contains_key(&func) { + let rs = ReuseDistanceState::new(trace, &func) + .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; + reuse_distance_renderers.insert(func.clone(), rs); + } + let rs = reuse_distance_renderers + .get_mut(&func) + .expect("just inserted"); + + let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); + let load_indices = trace.func_load_indices(&func).unwrap_or(&[]); + let store_k = store_indices.partition_point(|&p| p <= global_index as usize); + let load_k = load_indices.partition_point(|&p| p <= global_index as usize); + rs.seek(trace, store_indices, load_indices, store_k, load_k); + + Ok(Response::new(rs.to_rgba())) +} diff --git a/apps/halidoscope/frontend/src-tauri/src/lib.rs b/apps/halidoscope/frontend/src-tauri/src/lib.rs index 90cd43dfb853..b246862855bb 100644 --- a/apps/halidoscope/frontend/src-tauri/src/lib.rs +++ b/apps/halidoscope/frontend/src-tauri/src/lib.rs @@ -29,7 +29,8 @@ pub fn run() { commands::open_trace, commands::render_at, commands::render_heatmap, - commands::render_redundant + commands::render_redundant, + commands::render_reuse_distance ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/apps/halidoscope/frontend/src-tauri/src/render.rs b/apps/halidoscope/frontend/src-tauri/src/render.rs index 88df07ab572d..c6b57c0ec2cb 100644 --- a/apps/halidoscope/frontend/src-tauri/src/render.rs +++ b/apps/halidoscope/frontend/src-tauri/src/render.rs @@ -179,6 +179,7 @@ pub struct RedundantState { last_values: Vec>, /// Redundant-store count per spatial pixel, indexed by `y * width + x`. redundant_counts: Vec, + global_max_redundant_count: i32, applied_k: usize, } @@ -187,10 +188,17 @@ impl RedundantState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let n_pixels = geom.width * geom.height; + let global_max_redundant_count = trace + .funcs + .values() + .map(|s| s.max_redundant_count) + .max() + .unwrap_or(0); Some(Self { geom, last_values: vec![None; n_pixels * geom.channels], redundant_counts: vec![0i32; n_pixels], + global_max_redundant_count, applied_k: 0, }) } @@ -257,22 +265,17 @@ impl RedundantState { /// Produces a `width × height × 4` RGBA8 buffer. Pixels with zero redundant stores are black; /// pixels with one or more are mapped through the Reds colormap, normalized against the - /// per-Func full-trace maximum so the scale is stable while scrubbing. + /// global full-trace maximum so intensities are comparable across all Funcs. pub fn to_rgba(&self) -> Vec { - let FuncGeometry { - width, - height, - max_redundant_count, - .. - } = self.geom; + let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { let c = gradient.eval_continuous(i as f64 / 255.0); [c.r, c.g, c.b] }); - let scale = if max_redundant_count > 0 { - 255.0 / max_redundant_count as f64 + let scale = if self.global_max_redundant_count > 0 { + 255.0 / self.global_max_redundant_count as f64 } else { 0.0 }; @@ -303,6 +306,8 @@ pub struct HeatmapState { geom: FuncGeometry, mode: HeatmapMode, counts: Vec, + global_max_store_count: i32, + global_max_load_count: i32, applied_k: usize, } @@ -311,10 +316,24 @@ impl HeatmapState { pub fn new(trace: &Trace, func: &str, mode: HeatmapMode) -> Option { let geom = trace.func_geometry(func)?; let counts = vec![0i32; geom.width * geom.height]; + let global_max_store_count = trace + .funcs + .values() + .map(|s| s.max_store_count) + .max() + .unwrap_or(0); + let global_max_load_count = trace + .funcs + .values() + .map(|s| s.max_load_count) + .max() + .unwrap_or(0); Some(Self { geom, mode, counts, + global_max_store_count, + global_max_load_count, applied_k: 0, }) } @@ -366,19 +385,13 @@ impl HeatmapState { } /// Produces a `width × height × 4` RGBA8 buffer with the inferno colormap applied. Counts are - /// normalized against the per-Func full-trace maximum so the scale is consistent as the - /// playhead moves. + /// normalized against the global full-trace maximum so intensities are comparable across all + /// Funcs. pub fn to_rgba(&self) -> Vec { - let FuncGeometry { - width, - height, - max_store_count, - max_load_count, - .. - } = self.geom; + let FuncGeometry { width, height, .. } = self.geom; let max_count = match self.mode { - HeatmapMode::Stores => max_store_count, - HeatmapMode::Loads => max_load_count, + HeatmapMode::Stores => self.global_max_store_count, + HeatmapMode::Loads => self.global_max_load_count, }; // Build a 256-entry LUT once before the pixel loop. Calling eval_continuous 256 times is @@ -407,3 +420,219 @@ impl HeatmapState { out } } + +// ── Reuse distance rendering ────────────────────────────────────────────────── + +/// Per-pixel maximum reuse distance for one Func, seekable along the global timeline. +/// +/// For intermediate Funcs (those with stores) the anchor is the most recent store per +/// `(x, y, channel)`; reuse distance is measured to the next load from the same location. +/// +/// For pipeline inputs (loads only, no stores) the anchor is the *first* load per location — +/// treating that load as a free "memcpy" — and subsequent loads measure distance from it. +/// +/// Both the store and load index lists are merged in global order during seeking. +/// Backward seeks reset and replay from zero. +pub struct ReuseDistanceState { + geom: FuncGeometry, + /// Whether this Func is a pipeline input (no store events in the trace). + is_input: bool, + /// Per `(x, y, channel)` anchor, flat row-major: + /// `anchor_at[(y * width + x) * channels + c]`. + /// For intermediate Funcs: global index of the most recent store (`usize::MAX` = none yet). + /// For inputs: global index of the first load (`usize::MAX` = none yet). + anchor_at: Vec, + /// Maximum observed reuse distance per spatial pixel, indexed by `y * width + x`. + max_reuse_distance: Vec, + /// Trace-wide maximum reuse distance, used to normalize the color scale consistently across + /// all Funcs regardless of which one is being viewed. + global_max_reuse_distance: i64, + /// Number of this Func's store events processed. + applied_store_k: usize, + /// Number of this Func's load events processed. + applied_load_k: usize, +} + +impl ReuseDistanceState { + /// Builds an empty reuse distance state for `func`, or `None` if the Func has no usable + /// geometry. + pub fn new(trace: &Trace, func: &str) -> Option { + let geom = trace.func_geometry(func)?; + let n_cells = geom.width * geom.height * geom.channels; + let is_input = trace.func_store_indices(func).map_or(true, |s| s.is_empty()); + let global_max_reuse_distance = trace + .funcs + .values() + .map(|s| s.max_reuse_distance) + .max() + .unwrap_or(0); + Some(Self { + geom, + is_input, + anchor_at: vec![usize::MAX; n_cells], + max_reuse_distance: vec![0i64; geom.width * geom.height], + global_max_reuse_distance, + applied_store_k: 0, + applied_load_k: 0, + }) + } + + fn reset(&mut self) { + self.anchor_at.iter_mut().for_each(|v| *v = usize::MAX); + self.max_reuse_distance.iter_mut().for_each(|d| *d = 0); + self.applied_store_k = 0; + self.applied_load_k = 0; + } + + /// Seeks to the state after the first `target_store_k` stores and `target_load_k` loads. + /// Events are replayed in global packet order via a two-pointer merge of the two sorted index + /// lists. Backward seeks (either counter regresses) reset and replay from zero. + pub fn seek( + &mut self, + trace: &Trace, + store_indices: &[usize], + load_indices: &[usize], + target_store_k: usize, + target_load_k: usize, + ) { + let target_store_k = target_store_k.min(store_indices.len()); + let target_load_k = target_load_k.min(load_indices.len()); + + if target_store_k < self.applied_store_k || target_load_k < self.applied_load_k { + self.reset(); + } + + let store_slice = &store_indices[self.applied_store_k..target_store_k]; + let load_slice = &load_indices[self.applied_load_k..target_load_k]; + let mut si = 0; + let mut li = 0; + + while si < store_slice.len() || li < load_slice.len() { + let next_is_store = si < store_slice.len() + && (li >= load_slice.len() || store_slice[si] < load_slice[li]); + + if next_is_store { + self.apply_store(&trace.packets[store_slice[si]], store_slice[si]); + si += 1; + } else { + self.apply_load(&trace.packets[load_slice[li]], load_slice[li]); + li += 1; + } + } + + self.applied_store_k = target_store_k; + self.applied_load_k = target_load_k; + } + + fn apply_store(&mut self, pkt: &TracePacket, global_idx: usize) { + // Pipeline inputs have no stores; skip to avoid a stale anchor being set. + if self.is_input { + return; + } + let FuncGeometry { + width, + height, + channels, + min_x, + min_y, + min_c, + .. + } = self.geom; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for lane in 0..n_lanes { + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= width || y as usize >= height { + continue; + } + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + if c < 0 || c as usize >= channels { + continue; + } + let val_idx = (y as usize * width + x as usize) * channels + c as usize; + self.anchor_at[val_idx] = global_idx; + } + } + + fn apply_load(&mut self, pkt: &TracePacket, global_idx: usize) { + let FuncGeometry { + width, + height, + channels, + min_x, + min_y, + min_c, + .. + } = self.geom; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for lane in 0..n_lanes { + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= width || y as usize >= height { + continue; + } + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + if c < 0 || c as usize >= channels { + continue; + } + let val_idx = (y as usize * width + x as usize) * channels + c as usize; + let pixel_idx = y as usize * width + x as usize; + if self.is_input { + // First load is the free memcpy; establish the anchor and record no distance. + // Subsequent loads to the same location measure from that first load. + if self.anchor_at[val_idx] == usize::MAX { + self.anchor_at[val_idx] = global_idx; + } else { + let dist = (global_idx - self.anchor_at[val_idx]) as i64; + if dist > self.max_reuse_distance[pixel_idx] { + self.max_reuse_distance[pixel_idx] = dist; + } + } + } else if self.anchor_at[val_idx] != usize::MAX { + let dist = (global_idx - self.anchor_at[val_idx]) as i64; + if dist > self.max_reuse_distance[pixel_idx] { + self.max_reuse_distance[pixel_idx] = dist; + } + } + } + } + + /// Produces a `width × height × 4` RGBA8 buffer. Pixels with no observed store→load pair are + /// black; positive distances map through the Inferno colormap normalized against the per-Func + /// full-trace maximum so the scale is stable while scrubbing. + pub fn to_rgba(&self) -> Vec { + let FuncGeometry { width, height, .. } = self.geom; + + let gradient = colorous::INFERNO; + let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { + let c = gradient.eval_continuous(i as f64 / 255.0); + [c.r, c.g, c.b] + }); + let scale = if self.global_max_reuse_distance > 0 { + 255.0 / self.global_max_reuse_distance as f64 + } else { + 0.0 + }; + + let mut out = vec![0u8; width * height * 4]; + for (chunk, &dist) in out.chunks_exact_mut(4).zip(self.max_reuse_distance.iter()) { + if dist > 0 { + let ti = (dist as f64 * scale) as usize; + let [r, g, b] = lut[ti.min(255)]; + chunk[0] = r; + chunk[1] = g; + chunk[2] = b; + } + chunk[3] = 255; + } + out + } +} diff --git a/apps/halidoscope/frontend/src-tauri/src/trace.rs b/apps/halidoscope/frontend/src-tauri/src/trace.rs index b582fe8f35b8..b27d1d369e9c 100644 --- a/apps/halidoscope/frontend/src-tauri/src/trace.rs +++ b/apps/halidoscope/frontend/src-tauri/src/trace.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; -// ── Type system ─────────────────────────────────────────────────────────────── +// ── Type system ────────────────────────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TypeCode { @@ -33,21 +33,20 @@ pub struct HalideType { } impl HalideType { - // Obtain the number of bytes for a single scalar element (i.e., one lane) - // of a packet's value. For sub-byte types, this rounds up to the nearest - // whole byte. + // Obtain the number of bytes for a single scalar element (i.e., one SIMD lane) of a packet's + // value. For sub-byte types, this rounds up to the nearest whole byte. pub fn elem_bytes(self) -> usize { (self.bits as usize + 7) / 8 } - // Obtain the number of bytes for the entire value of a packet. - // This is the product of the number of lanes and the size of each lane. + // Obtain the number of bytes for the entire value of a packet. This is the product of the + // number of lanes and the size of each lane. pub fn value_bytes(self) -> usize { self.lanes as usize * self.elem_bytes() } } -// ── Event codes ─────────────────────────────────────────────────────────────── +// ── Event codes ────────────────────────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EventCode { @@ -93,8 +92,7 @@ pub struct TracePacket { pub parent_id: i32, pub value_index: i32, pub type_: HalideType, - /// Coordinates in dim-major / lane-minor order: - /// [x₀..xₙ, y₀..yₙ, c₀..cₙ] where n = type_.lanes. + /// Coordinates in dim-major / lane-minor order: [x₀..xₙ, y₀..yₙ, c₀..cₙ] where n = type_.lanes. pub coordinates: Vec, pub value: Vec, pub func: String, @@ -114,19 +112,20 @@ impl TracePacket { self.is_load() || self.is_store() } - /// Decodes lane `lane` of this packet's value into an `f64`. Returns `None` - /// when the type isn't a decodable numeric (handle/unknown/odd bit width) - /// or when the lane runs past the value bytes. All numeric types collapse to - /// `f64` so callers have a single comparable scalar. + /// Decodes lane `lane` of this packet's value into an `f64`. Returns `None` when the type isn't + /// a decodable numeric (Handle / Unknown / an odd bit width) or when the lane runs past the + /// value bytes. All numeric types collapse to `f64` so callers have a single comparable scalar. pub fn decoded_value(&self, lane: usize) -> Option { let elem_bytes = self.type_.elem_bytes(); if elem_bytes == 0 { return None; } + let off = lane * elem_bytes; if off + elem_bytes > self.value.len() { return None; } + let s = &self.value[off..]; match (self.type_.code, self.type_.bits) { (TypeCode::Float, 32) => Some(f32::from_le_bytes(s[..4].try_into().unwrap()) as f64), @@ -158,15 +157,35 @@ pub struct FuncStats { pub max_coords: Vec, pub min_value: Option, pub max_value: Option, + /// Maximum number of stores observed at any array / tensor coordinate for this Func. pub max_store_count: i32, + /// Maximum number of loads observed at any array / tensor coordinate for this Func. pub max_load_count: i32, + /// Maximum number of redundant stores observed at any array / tensor coordinate for this Func. + /// A store is considered redundant when the incoming value bit-matches the previously stored + /// value at that location. pub max_redundant_count: i32, - /// Frequency distribution of per-pixel store counts: `hist[k]` is the number of pixel + /// Maximum store-to-load distance observed across all array / tensor coordinates for this Func. + /// Measured as thedifference in global packet indices between a store and the next load from + /// the same coordination. 0 when no store→load pair was observed. + pub max_reuse_distance: i64, + /// Frequency distribution of per-coordinate store counts. `hist[k]` is the number of pixel /// locations stored exactly `k` times, for `k` in `0..=max_store_count`. The `0` bin is /// included. Empty when the Func has no usable extent. pub store_count_histogram: Vec, + /// Frequency distribution of per-coordinate load counts. `hist[k]` is the number of pixel + /// locations loaded exactly `k` times, for `k` in `0..=max_load_count`. The `0` bin is + /// included. Empty when the Func has no usable extent. pub load_count_histogram: Vec, + /// Frequency distribution of per-coordinate redundant store counts. `hist[k]` is the number of + /// pixel locations with exactly `k` redundant stores, for `k` in `0..=max_redundant_count`. + /// The `0` bin is included. Empty when the Func has no usable extent. pub redundant_count_histogram: Vec, + /// Fixed-width 64-bucket histogram of per-coordinate maximum reuse distances. Bucket `k` covers + /// distances in `[k/63 * max, (k+1)/63 * max)`, with bucket 63 inclusive of `max`. Pixels + /// with no observed store→load pair (distance 0) are excluded. Empty when + /// `max_reuse_distance == 0`. + pub reuse_distance_histogram: Vec, } impl Default for FuncStats { @@ -180,18 +199,16 @@ impl Default for FuncStats { max_store_count: 0, max_load_count: 0, max_redundant_count: 0, + max_reuse_distance: 0, store_count_histogram: vec![], load_count_histogram: vec![], redundant_count_histogram: vec![], + reuse_distance_histogram: vec![], } } } -/// Full spatial layout of a Func: pixel dimensions plus the channel axis -/// (logical dim 2). Single source of truth for geometry, shared by the -/// renderer and by frontend-metadata derivation, so canvas sizing can never -/// disagree between them. All extents use the half-open `[min, max)` -/// convention that the coordinate accumulation establishes. +/// Full spatial layout of a Func: pixel dimensions plus the channel axis (logical dim 2). #[derive(Debug, Clone, Copy)] pub struct FuncGeometry { pub width: usize, @@ -203,13 +220,13 @@ pub struct FuncGeometry { pub max_store_count: i32, pub max_load_count: i32, pub max_redundant_count: i32, + pub max_reuse_distance: i64, } // ── Complete trace ──────────────────────────────────────────────────────────── -// Note: We use BTreeMaps for deterministic iteration order here. We could -// consider switching to HashMaps to get O(1) lookups if we find funcs lookup -// start to become a bottleneck. +// Note: We use BTreeMaps for deterministic iteration order here. We could consider switching to +// HashMaps to get O(1) lookups if we find Func lookup starts to become a bottleneck. pub struct Trace { pub packets: Vec, pub funcs: BTreeMap, @@ -217,6 +234,7 @@ pub struct Trace { pub dag_edges: BTreeMap>, pub store_indices_by_func: BTreeMap>, pub load_indices_by_func: BTreeMap>, + pub liveness_range_by_func: BTreeMap, } // ── Binary parsing helpers ──────────────────────────────────────────────────── @@ -259,8 +277,12 @@ fn u16_le(buf: &[u8], off: usize) -> u16 { /// Read a null-terminated C string. Returns `(string, bytes_consumed_including_null)`. fn read_cstr(buf: &[u8]) -> (&str, usize) { - let null = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); - (std::str::from_utf8(&buf[..null]).unwrap_or(""), null + 1) + let null_idx = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + + ( + std::str::from_utf8(&buf[..null_idx]).unwrap_or(""), + null_idx + 1, + ) } // ── Stats helpers called during packet parsing ──────────────────────────────── @@ -280,28 +302,27 @@ fn update_coord_range(pkt: &TracePacket, stats: &mut FuncStats) { if stats.min_coords.is_empty() { stats.min_coords.resize(logical_dims, 0); stats.max_coords.resize(logical_dims, 0); + for d in 0..logical_dims { let mut mn = pkt.coordinates[d * n_lanes]; let mut mx = mn + 1; + for l in 1..n_lanes { - let c = pkt.coordinates[d * n_lanes + l]; - mn = mn.min(c); - mx = mx.max(c + 1); + let coord = pkt.coordinates[d * n_lanes + l]; + mn = mn.min(coord); + mx = mx.max(coord + 1); } + stats.min_coords[d] = mn; stats.max_coords[d] = mx; } } else { - let dims = logical_dims.min(stats.min_coords.len()); - for d in 0..dims { + for d in 0..logical_dims { for l in 0..n_lanes { - let c = pkt.coordinates[d * n_lanes + l]; - if c < stats.min_coords[d] { - stats.min_coords[d] = c; - } - if c + 1 > stats.max_coords[d] { - stats.max_coords[d] = c + 1; - } + let coord = pkt.coordinates[d * n_lanes + l]; + + stats.min_coords[d] = stats.min_coords[d].min(coord); + stats.max_coords[d] = stats.max_coords[d].max(coord + 1); } } } @@ -342,25 +363,27 @@ fn parse_func_type_and_dim( let mut tokens = trace_tag.split_whitespace(); tokens.next(); // consume "func_type_and_dim:" - // Skip over the type descriptions, which we don't currently use. - // We could consider using them in the future to populate FuncStats.type if that'd be a - // useful addition. + // Skip over the type descriptions, which we don't currently use. We could consider using them + // in the future to populate FuncStats.type if that'd be a useful addition. let num_types: usize = match tokens.next().and_then(|s| s.parse().ok()) { Some(n) => n, None => return, }; + for _ in 0..num_types * 3 { tokens.next(); } - // Parse the dimension descriptions to extract the overall min and max - // coordinates for the Func. + // Parse the dimension descriptions to extract the overall min and max coordinates for the Func. let num_dims: usize = match tokens.next().and_then(|s| s.parse().ok()) { Some(n) => n, None => return, }; + + // Pre-allocate the min/max coordinate vectors to avoid repeated reallocations during parsing. let mut min_coords = Vec::with_capacity(num_dims); let mut max_coords = Vec::with_capacity(num_dims); + for _ in 0..num_dims { let min: i32 = match tokens.next().and_then(|s| s.parse().ok()) { Some(v) => v, @@ -370,6 +393,7 @@ fn parse_func_type_and_dim( Some(v) => v, None => break, }; + min_coords.push(min); max_coords.push(min + extent); } @@ -379,7 +403,7 @@ fn parse_func_type_and_dim( // (declared realization bounds) and update_coord_range (coords observed on Load/Store). // // In practice Halide emits this tag at pipeline start, before any load/store, so the common - //path is "tag seeds, accesses expand." + // path is "tag seeds, accesses expand." if !min_coords.is_empty() { let entry = funcs.entry(qualified.to_owned()).or_default(); entry.min_coords = min_coords; @@ -405,16 +429,20 @@ impl Trace { let mut dag_edges: BTreeMap> = BTreeMap::new(); let mut store_indices_by_func: BTreeMap> = BTreeMap::new(); let mut load_indices_by_func: BTreeMap> = BTreeMap::new(); + let mut liveness_range_by_func: BTreeMap = BTreeMap::new(); - // id -> pipeline name: propagated down the parent chain so every event - // in a pipeline can compute its qualified name. + // id -> pipeline name: propagated down the parent chain so every event in a pipeline can + // compute its qualified name. let mut parent_to_pipeline: HashMap = HashMap::new(); - // id -> (event, qualified_name, parent_id): needed for DAG inference after - // all packets are parsed. + + // id -> (event, qualified_name, parent_id): needed for DAG inference after all packets are + // parsed. let mut id_to_info: HashMap = HashMap::new(); + // Loads we deferred for DAG inference. let mut pending_loads: Vec<(String, i32)> = Vec::new(); + // Packet parsing loop. while pos + HEADER_BYTES <= total { let size = u32_le(data, pos) as usize; if size < HEADER_BYTES || pos + size > total { @@ -449,28 +477,26 @@ impl Trace { .map(|i| i32_le(pkt_data, coords_off + i * 4)) .collect(); - let value = if value_off + value_len <= pkt_data.len() { - pkt_data[value_off..value_off + value_len].to_vec() - } else { - vec![] - }; + let value = pkt_data + .get(value_off..value_off + value_len) + .map(|s| s.to_vec()) + .unwrap_or_default(); - let (func_name, func_len) = if func_off < pkt_data.len() { - let (s, n) = read_cstr(&pkt_data[func_off..]); - (s.to_owned(), n) - } else { - (String::new(), 0) - }; + let (func_name, func_len) = pkt_data + .get(func_off..) + .map(|s| { + let (name, n) = read_cstr(s); + (name.to_owned(), n) + }) + .unwrap_or_default(); let tag_off = func_off + func_len; - let trace_tag = if tag_off < pkt_data.len() { - let (s, _) = read_cstr(&pkt_data[tag_off..]); - s.to_owned() - } else { - String::new() - }; + let trace_tag = pkt_data + .get(tag_off..) + .map(|s| read_cstr(s).0.to_owned()) + .unwrap_or_default(); - // ── Pipeline context propagation ────────────────────────────────── + // ── Pipeline context propagation ───────────────────────────────────────────────────── match ev { EventCode::BeginPipeline => { pipelines.insert(id, func_name.clone()); @@ -480,20 +506,23 @@ impl Trace { parent_to_pipeline.remove(&parent_id); } _ => { + // Propagate the pipeline name down the parent chain so every event can compute + // the pipeline it belongs to. if let Some(pl) = parent_to_pipeline.get(&parent_id).cloned() { parent_to_pipeline.insert(id, pl); } } } - // ── Qualified name ──────────────────────────────────────────────── + // ── Qualified name ─────────────────────────────────────────────────────────────────── let qualified = match parent_to_pipeline.get(&parent_id) { Some(pl) if !pl.is_empty() => format!("{}:{}", pl, func_name), _ => func_name.clone(), }; + id_to_info.insert(id, (ev, qualified.clone(), parent_id)); - // ── Build the packet ────────────────────────────────────────────── + // ── Build the packet ───────────────────────────────────────────────────────────────── let pkt = TracePacket { id, event: ev, @@ -516,6 +545,23 @@ impl Trace { } EventCode::BeginRealization => { funcs.entry(qualified.clone()).or_default(); + + // Start the liveness range for this Func at the current packet index. + let idx = packets.len() as u32; + + liveness_range_by_func + .entry(qualified.clone()) + .and_modify(|range| range.0 = range.0.min(idx)) + .or_insert((idx, idx)); + } + EventCode::EndRealization => { + // End the liveness range for this Func at the current packet index. + let idx = packets.len() as u32; + + liveness_range_by_func + .entry(qualified.clone()) + .and_modify(|range| range.1 = range.1.max(idx)) + .or_insert((idx, idx)); } EventCode::Load => { // When we observe a load event, add its current index (equivalent to @@ -556,9 +602,9 @@ impl Trace { pos += size; } - // ── DAG inference ───────────────────────────────────────────────────── - // Walk up the parent chain from each load to find the enclosing Produce - // event; that Produce's func is a producer of the loaded func. + // ── DAG inference ──────────────────────────────────────────────────────────────────────── + // Walk up the parent chain from each load to find the enclosing Produce event; that + // Produce's func is a producer of the loaded func. for (func_name, load_parent_id) in &pending_loads { let loaded_func = match parent_to_pipeline.get(load_parent_id) { Some(pl) if !pl.is_empty() => format!("{}:{}", pl, func_name), @@ -689,6 +735,191 @@ impl Trace { } } + // Compute max per-pixel reuse distance for each Func. Two separate loops handle the two + // cases: + // + // 1. Intermediate Funcs (have stores): anchor = most recent store; distance measured to + // the next load from the same (x, y, channel). Events are two-pointer merged in + // global order. + // + // 2. Pipeline inputs (loads only, no stores): the first load at each pixel is a memcpy + // and is "free". Subsequent loads to the same pixel measure distance from that first + // load. Black = only one load ever (no reuse). + // + // Per-pixel distance vecs are collected here; histogram building is deferred until the + // global max is known so all Funcs share the same bucket scale. + let mut reuse_distances_by_func: BTreeMap> = BTreeMap::new(); + for (qualified, store_indices) in &store_indices_by_func { + let extents = funcs.get(qualified.as_str()).and_then(func_extents); + if let Some((w, h, min_x, min_y)) = extents { + let stats = funcs.get(qualified.as_str()).unwrap(); + let (channels, min_c) = if stats.min_coords.len() >= 3 { + ( + (stats.max_coords[2] - stats.min_coords[2]).max(1) as usize, + stats.min_coords[2], + ) + } else { + (1, 0) + }; + let load_indices = load_indices_by_func + .get(qualified.as_str()) + .map(Vec::as_slice) + .unwrap_or(&[]); + + // usize::MAX = no store has landed at this (x, y, channel) yet. + let mut last_store_at = vec![usize::MAX; w * h * channels]; + let mut max_reuse_distances = vec![0i64; w * h]; + let mut si = 0; + let mut li = 0; + + while si < store_indices.len() || li < load_indices.len() { + let next_is_store = si < store_indices.len() + && (li >= load_indices.len() || store_indices[si] < load_indices[li]); + + if next_is_store { + let global_idx = store_indices[si]; + si += 1; + let pkt = &packets[global_idx]; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for lane in 0..n_lanes { + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= w || y as usize >= h { + continue; + } + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + if c < 0 || c as usize >= channels { + continue; + } + let val_idx = (y as usize * w + x as usize) * channels + c as usize; + last_store_at[val_idx] = global_idx; + } + } else { + let global_idx = load_indices[li]; + li += 1; + let pkt = &packets[global_idx]; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for lane in 0..n_lanes { + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= w || y as usize >= h { + continue; + } + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + if c < 0 || c as usize >= channels { + continue; + } + let val_idx = (y as usize * w + x as usize) * channels + c as usize; + let pixel_idx = y as usize * w + x as usize; + if last_store_at[val_idx] != usize::MAX { + let dist = (global_idx - last_store_at[val_idx]) as i64; + if dist > max_reuse_distances[pixel_idx] { + max_reuse_distances[pixel_idx] = dist; + } + } + } + } + } + + if let Some(stats) = funcs.get_mut(qualified.as_str()) { + stats.max_reuse_distance = + max_reuse_distances.iter().copied().max().unwrap_or(0); + } + reuse_distances_by_func.insert(qualified.clone(), max_reuse_distances); + } + } + + // Pipeline inputs: Funcs with loads but no stores. The first load at each (x, y, channel) + // is free (analogous to a memcpy). Subsequent loads measure distance from that first load. + for (qualified, load_indices) in &load_indices_by_func { + if store_indices_by_func.contains_key(qualified.as_str()) { + continue; // handled by the store-anchor loop above + } + let extents = funcs.get(qualified.as_str()).and_then(func_extents); + if let Some((w, h, min_x, min_y)) = extents { + let stats = funcs.get(qualified.as_str()).unwrap(); + let (channels, min_c) = if stats.min_coords.len() >= 3 { + ( + (stats.max_coords[2] - stats.min_coords[2]).max(1) as usize, + stats.min_coords[2], + ) + } else { + (1, 0) + }; + + // usize::MAX = first load hasn't occurred at this (x, y, channel) yet. + let mut first_load_at = vec![usize::MAX; w * h * channels]; + let mut max_reuse_distances = vec![0i64; w * h]; + + for &global_idx in load_indices { + let pkt = &packets[global_idx]; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for lane in 0..n_lanes { + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= w || y as usize >= h { + continue; + } + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + if c < 0 || c as usize >= channels { + continue; + } + let val_idx = (y as usize * w + x as usize) * channels + c as usize; + let pixel_idx = y as usize * w + x as usize; + if first_load_at[val_idx] == usize::MAX { + first_load_at[val_idx] = global_idx; + } else { + let dist = (global_idx - first_load_at[val_idx]) as i64; + if dist > max_reuse_distances[pixel_idx] { + max_reuse_distances[pixel_idx] = dist; + } + } + } + } + + if let Some(stats) = funcs.get_mut(qualified.as_str()) { + stats.max_reuse_distance = + max_reuse_distances.iter().copied().max().unwrap_or(0); + } + reuse_distances_by_func.insert(qualified.clone(), max_reuse_distances); + } + } + + // Build globally-normalized 64-bucket histograms: bucket boundaries are identical across + // all Funcs so the x-axis is directly comparable. + let global_max_reuse_distance: i64 = funcs + .values() + .map(|s| s.max_reuse_distance) + .max() + .unwrap_or(0); + if global_max_reuse_distance > 0 { + for (qualified, distances) in &reuse_distances_by_func { + let mut hist = vec![0u32; 64]; + for &dist in distances { + if dist > 0 { + let bucket = + (dist as f64 / global_max_reuse_distance as f64 * 63.0) as usize; + hist[bucket] += 1; + } + } + if let Some(stats) = funcs.get_mut(qualified.as_str()) { + stats.reuse_distance_histogram = hist; + } + } + } + Ok(Self { packets, funcs, @@ -696,6 +927,7 @@ impl Trace { dag_edges, store_indices_by_func, load_indices_by_func, + liveness_range_by_func, }) } @@ -712,6 +944,10 @@ impl Trace { self.load_indices_by_func.get(qualified).map(Vec::as_slice) } + pub fn func_liveness_range(&self, qualified: &str) -> Option<&(u32, u32)> { + self.liveness_range_by_func.get(qualified) + } + /// Spatial layout for `qualified`, or `None` if it has no usable coordinate extent. Reuses /// `func_extents` for pixel dims so the renderer and the metadata layer agree, and adds the /// channel axis (logical dim 2). @@ -736,6 +972,7 @@ impl Trace { max_store_count: stats.max_store_count, max_load_count: stats.max_load_count, max_redundant_count: stats.max_redundant_count, + max_reuse_distance: stats.max_reuse_distance, }) } } diff --git a/apps/halidoscope/frontend/src/App.css b/apps/halidoscope/frontend/src/App.css index 6575625aed1e..9faa1b664070 100644 --- a/apps/halidoscope/frontend/src/App.css +++ b/apps/halidoscope/frontend/src/App.css @@ -40,6 +40,7 @@ input[type="number"] { --color-ps-border-primary: oklch(0.3407 0 0); --color-ps-border-secondary: oklch(0.3979 0 0); --color-ps-border-tertiary: oklch(0.4997 0 0); + --color-highlight: oklch(0.77 0.1919 163.7); --text-tiny: 0.625rem; --text-tiny--line-height: 1.5; @@ -47,7 +48,8 @@ input[type="number"] { @layer components { .text-responsive { - font-size: clamp(0.5rem, calc(1rem / var(--zoom-level)), 1.5rem); + font-size: min(calc(0.75rem / var(--zoom-level)), 0.75rem); + line-height: min(calc(1rem / var(--zoom-level)), 1rem); } @keyframes slideDown { diff --git a/apps/halidoscope/frontend/src/App.tsx b/apps/halidoscope/frontend/src/App.tsx index 2fdbdd42c3bc..646395252939 100644 --- a/apps/halidoscope/frontend/src/App.tsx +++ b/apps/halidoscope/frontend/src/App.tsx @@ -20,6 +20,8 @@ function App() { const [globalMaxLoadCount, setGlobalMaxLoadCount] = React.useState(0); const [globalMaxRedundantCount, setGlobalMaxRedundantCount] = React.useState(0); + const [globalMaxReuseDistance, setGlobalMaxReuseDistance] = + React.useState(0); const setActiveFunc = useSetAtom(funcAtom); @@ -44,6 +46,7 @@ function App() { global_max_store_count, global_max_load_count, global_max_redundant_count, + global_max_reuse_distance, } = await openTrace(resolved); const byName: Record = {}; @@ -57,6 +60,7 @@ function App() { setGlobalMaxStoreCount(global_max_store_count); setGlobalMaxLoadCount(global_max_load_count); setGlobalMaxRedundantCount(global_max_redundant_count); + setGlobalMaxReuseDistance(global_max_reuse_distance); setActiveFunc(funcs[0]?.name ?? ""); } catch (err) { console.error("Error loading trace from CLI: ", err); @@ -75,6 +79,7 @@ function App() { globalMaxStoreCount, globalMaxLoadCount, globalMaxRedundantCount, + globalMaxReuseDistance, }} >
diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/FuncSelect.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/FuncSelect.tsx deleted file mode 100644 index 5ce4bb191e6a..000000000000 --- a/apps/halidoscope/frontend/src/components/controls/visualizations/FuncSelect.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Label, Select } from "radix-ui"; -import { useAtom } from "jotai"; - -import { useTraceContext } from "@/hooks/trace"; -import { funcAtom } from "@/state/func"; - -function FuncSelect() { - const { funcs } = useTraceContext(); - const [activeFunc, setActiveFunc] = useAtom(funcAtom); - - return ( -
- Selected Func - setActiveFunc(value)} - > - - - - - - - - - - - {Object.keys(funcs).map((func) => ( - - {func} - - ))} - - - -
- ); -} - -export default FuncSelect; diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/GraphDisplay.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/GraphDisplay.tsx new file mode 100644 index 000000000000..df367658cddb --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/GraphDisplay.tsx @@ -0,0 +1,56 @@ +import { type Edge } from "@xyflow/react"; +import { useSetAtom } from "jotai"; +import { Checkbox } from "radix-ui"; +import * as React from "react"; + +import { edgesAtom } from "@/state/graph"; + +function hideEdge(hidden: boolean) { + return function handleVisibilityChange(edge: Edge) { + return { + ...edge, + hidden, + }; + }; +} + +function GraphDisplay() { + const [visible, setVisible] = React.useState(true); + const setEdges = useSetAtom(edgesAtom); + + function onEdgeVisibilityChange(checked: boolean) { + setVisible(checked); + setEdges((eds) => eds.map(hideEdge(!checked))); + } + + return ( +
+ + + + + + + + +
+ ); +} + +export default GraphDisplay; diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx index c7a66afc230a..dbf01b298f47 100644 --- a/apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx @@ -1,25 +1,32 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; +import { useAtomValue } from "jotai"; import * as React from "react"; -const RAMP_STOPS = 32; -const STOPS = Array.from({ length: RAMP_STOPS + 1 }, (_, i) => { - const t = i / RAMP_STOPS; - return { offset: t, color: d3.rgb(d3.interpolateInferno(t)).formatHex() }; -}); -const RAMP_DY = 12; +import { histogramAtom, HistogramScale } from "@/state/histogram"; interface HistogramProps { - data: { x: number; y: number }[]; + data: { x1: number; x2: number; y: number }[]; + domain: [number, number]; labels: { x: string; }; } -function Histogram({ data, labels }: HistogramProps) { +function Histogram({ data, domain, labels }: HistogramProps) { const ref = React.useRef(null); - const gradient = React.useRef(null); - const rampId = `histogram-ramp-${React.useId().replace(/:/g, "")}`; + const histogramScale = useAtomValue(histogramAtom) as HistogramScale; + // Build the data for the bottom colorbar. + const colorbar = React.useMemo(() => { + const range = domain[1] - domain[0]; + const count = range <= 64 ? range : 64; + const step = range / count; + return new Array(count).fill(0).map((_, i) => ({ + x1: domain[0] + i * step, + x2: domain[0] + (i + 1) * step, + y: 0, + })); + }, [domain]); React.useEffect(() => { if (!ref.current) { @@ -33,87 +40,68 @@ function Histogram({ data, labels }: HistogramProps) { marginBottom: 60, y: { grid: true, - label: "Pixel Count", + label: "Coordinate Count", tickFormat: (value) => d3.format(".2s")(value), + ticks: 8, }, x: { + domain, label: labels.x, labelAnchor: "right", labelArrow: "right", - tickSize: 0, + tickFormat: (value) => d3.format(".2s")(value), tickPadding: 24, + tickSize: 0, + type: histogramScale, + interval: domain[1] <= 64 ? 1 : undefined, }, color: { + // Constrain the color scale to the bounds used for computing the canvas + // on the backend. + domain: [0, domain[1] - 1], scheme: "Inferno", + type: "linear", }, marks: [ - Plot.barY(data, { x: "x", y: "y", fill: "x" }), - Plot.ruleY([0], { - stroke: `url(#${rampId})`, + Plot.rectY(data, { x1: "x1", x2: "x2", y: "y", fill: "x1" }), + Plot.ruleY(colorbar, { + stroke: "x1", strokeWidth: 8, - dy: RAMP_DY, + x1: "x1", + x2: "x2", + y: 0, + dy: 12, }), ], }); ref.current.append(plot); - const xScale = plot.scale("x"); - if (xScale && gradient.current && data.length > 0) { - const bandwidth = xScale.bandwidth ?? 0; - const left = xScale.apply(data[0]!.x); - const right = xScale.apply(data[data.length - 1]!.x) + bandwidth; - - gradient.current.setAttribute("x1", String(left)); - gradient.current.setAttribute("x2", String(right)); - - // Plot draws the rule across the full frame; clip its rendered line(s) to the same - // footprint so the strip starts and ends with the bars rather than at the axes. The rule - // group is the only element stroked with our gradient, so we can find it by that url. - plot - .querySelector(`[stroke="url(#${rampId})"]`) - ?.querySelectorAll("line") - .forEach((line) => { - line.setAttribute("x1", String(left)); - line.setAttribute("x2", String(right)); - }); - } - return () => { plot.remove(); }; - }, [data, labels, rampId]); + }, [data, domain, labels, histogramScale, colorbar]); - return ( - <> + return data.every((d) => d.y === 0) ? ( +
-
- + No data to display +
+ ) : ( +
); } diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/HistogramSelect.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/HistogramSelect.tsx new file mode 100644 index 000000000000..27005b5dc5ed --- /dev/null +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/HistogramSelect.tsx @@ -0,0 +1,124 @@ +import { Label, Select } from "radix-ui"; +import { useAtom } from "jotai"; + +import { useTraceContext } from "@/hooks/trace"; +import { funcAtom } from "@/state/func"; +import { histogramAtom, type HistogramScale } from "@/state/histogram"; + +function HistogramSelect() { + const { funcs } = useTraceContext(); + const [activeFunc, setActiveFunc] = useAtom(funcAtom); + const [histogramScale, setHistogramScale] = useAtom(histogramAtom); + + return ( +
+
+ + Selected Func + + setActiveFunc(value)} + > + + + + + + + + + + + {Object.keys(funcs).map((func) => ( + + + {func} + + + ))} + + + +
+
+ + Scale + + setHistogramScale(value as HistogramScale)} + > + + + + + + + + + + + + Linear + + + Log + + + + +
+
+ ); +} + +export default HistogramSelect; diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx index 2fd6b877c565..7dd3367cc601 100644 --- a/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx @@ -1,12 +1,15 @@ import { useAtomValue } from "jotai"; +import * as React from "react"; import { Label, Separator } from "radix-ui"; -import FuncSelect from "@/components/controls/visualizations/FuncSelect"; +import GraphDisplay from "@/components/controls/visualizations/GraphDisplay"; import Histogram from "@/components/controls/visualizations/Histogram"; +import HistogramSelect from "@/components/controls/visualizations/HistogramSelect"; import PlaybackRate from "@/components/controls/visualizations/PlaybackRate"; import VisualizationSelect from "@/components/controls/visualizations/VisualizationsSelect"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; +import { histogramAtom, type HistogramScale } from "@/state/histogram"; import { type VisualizationMode, visualizationModeAtom, @@ -15,12 +18,13 @@ import { FuncMeta } from "@/types"; const VISUALIZATION_MODE_TO_HISTOGRAM_DATA_KEY: Record< VisualizationMode, - keyof FuncMeta + keyof FuncMeta | "" > = { "True Values": "", "Store Frequency": "store_count_histogram", "Load Frequency": "load_count_histogram", "Redundant Stores": "redundant_count_histogram", + "Reuse Distance": "reuse_distance_histogram", }; const VISUALIZATION_MODE_TO_LABEL: Record = { @@ -28,12 +32,77 @@ const VISUALIZATION_MODE_TO_LABEL: Record = { "Store Frequency": "Store Count", "Load Frequency": "Load Count", "Redundant Stores": "Redundant Store Count", + "Reuse Distance": "Reuse Distance (Packets)", }; function VisualizationsPanel() { - const { funcs } = useTraceContext(); + const { + funcs, + globalMaxStoreCount, + globalMaxLoadCount, + globalMaxRedundantCount, + globalMaxReuseDistance, + } = useTraceContext(); const visualizationMode = useAtomValue(visualizationModeAtom); - const func = useAtomValue(funcAtom); + const activeFunc = useAtomValue(funcAtom); + const histogramScale = useAtomValue(histogramAtom) as HistogramScale; + + const dataKey = VISUALIZATION_MODE_TO_HISTOGRAM_DATA_KEY[visualizationMode]; + const hasHistogram = dataKey && activeFunc && funcs[activeFunc]; + const domainMin = histogramScale === "log" ? 1 : 0; + + const { data: histogramData, domain: histogramDomain } = React.useMemo((): { + data: { x1: number; x2: number; y: number }[]; + domain: [number, number]; + } => { + if (!hasHistogram) { + return { data: [], domain: [domainMin, 1] }; + } + + const data = funcs[activeFunc][dataKey as keyof FuncMeta] as number[]; + + switch (visualizationMode) { + case "Store Frequency": + return { + data: data.map((pixels, i) => ({ x1: i, x2: i + 1, y: pixels })), + domain: [domainMin, globalMaxStoreCount + 1], + }; + case "Load Frequency": + return { + data: data.map((pixels, i) => ({ x1: i, x2: i + 1, y: pixels })), + domain: [domainMin, globalMaxLoadCount + 1], + }; + case "Redundant Stores": + return { + data: data.map((pixels, i) => ({ x1: i, x2: i + 1, y: pixels })), + domain: [domainMin, globalMaxRedundantCount + 1], + }; + // For Reuse Distance, scale x values to the global max reuse distance + // since the histogram is normalized to 64 bins. + case "Reuse Distance": + return { + data: data.map((pixels, i) => ({ + x1: Math.round((i / 64) * globalMaxReuseDistance), + x2: Math.round(((i + 1) / 64) * globalMaxReuseDistance), + y: pixels, + })), + domain: [domainMin, globalMaxReuseDistance], + }; + default: + return { data: [], domain: [domainMin, 1] }; + } + }, [ + hasHistogram, + activeFunc, + funcs, + dataKey, + visualizationMode, + domainMin, + globalMaxStoreCount, + globalMaxLoadCount, + globalMaxRedundantCount, + globalMaxReuseDistance, + ]); return (
@@ -46,31 +115,21 @@ function VisualizationsPanel() {
- {func && funcs[func] && visualizationMode !== "True Values" ? ( + {hasHistogram ? ( <>
Histogram - <> - +
+ ({ - x: stores, - y: pixels, - })) ?? [] - } + data={histogramData} + domain={histogramDomain} labels={{ x: VISUALIZATION_MODE_TO_LABEL[visualizationMode] }} /> - +
) : null} @@ -80,6 +139,7 @@ function VisualizationsPanel() { Parameters +
); diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx b/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx index 2c0dee4fe3cf..cf4d56ec7e6a 100644 --- a/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx +++ b/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx @@ -11,6 +11,7 @@ const VISUALIZATION_MODES = [ { value: "Store Frequency", label: "Store Frequency" }, { value: "Load Frequency", label: "Load Frequency" }, { value: "Redundant Stores", label: "Redundant Stores" }, + { value: "Reuse Distance", label: "Reuse Distance" }, ] as const; function VisualizationSelect() { diff --git a/apps/halidoscope/frontend/src/components/shared/Canvas.tsx b/apps/halidoscope/frontend/src/components/shared/Canvas.tsx index dcde7df6813f..919725cc3d8c 100644 --- a/apps/halidoscope/frontend/src/components/shared/Canvas.tsx +++ b/apps/halidoscope/frontend/src/components/shared/Canvas.tsx @@ -1,32 +1,25 @@ import { + applyEdgeChanges, ReactFlow, - useEdgesState, useNodesState, useViewport, type Node, type Edge, + type EdgeChange, } from "@xyflow/react"; -import { useSetAtom } from "jotai"; +import { useAtom, useSetAtom } from "jotai"; import * as React from "react"; import FuncCanvas from "@/components/views/tracer/FuncCanvas"; +import { funcAtom } from "@/state/func"; +import { edgesAtom } from "@/state/graph"; import { FuncMeta, NodeTypes } from "@/types"; import { buildEdges, buildNodes, getLayoutedElements } from "@/utils/graph"; -import { funcAtom } from "@/state/func"; const NODE_TYPES = { funcCanvas: FuncCanvas, }; -function hideEdge(hidden: boolean) { - return function handleVisibilityChange(edge: Edge) { - return { - ...edge, - hidden, - }; - }; -} - interface CanvasProps { funcs: Record; dagEdges: Record; @@ -40,20 +33,26 @@ function Canvas({ funcs, dagEdges, type }: CanvasProps) { const [nodes, _setNodes, onNodesChange] = useNodesState>(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - const [hidden, _setHidden] = React.useState(false); + const [edges, setEdges] = useAtom(edgesAtom); const setFunc = useSetAtom(funcAtom); + React.useEffect(() => { + setEdges(initialEdges); + }, [initialEdges, setEdges]); + + const onEdgesChange = React.useCallback( + (changes: EdgeChange[]) => { + setEdges((eds) => applyEdgeChanges(changes, eds)); + }, + [setEdges], + ); + const { zoom } = useViewport(); React.useEffect(() => { document.documentElement.style.setProperty("--zoom-level", zoom.toString()); }, [zoom]); - React.useEffect(() => { - setEdges((eds) => eds.map(hideEdge(hidden))); - }, [hidden, setEdges]); - return (
diff --git a/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx b/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx index 989290418c56..6da1763d636e 100644 --- a/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx +++ b/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx @@ -2,12 +2,14 @@ import { getIncomers, getOutgoers, Handle, - Position, type Node, type NodeProps, + Position, useEdges, useNodes, + useViewport, } from "@xyflow/react"; +import clsx from "clsx"; import { useAtomValue } from "jotai"; import * as React from "react"; @@ -15,14 +17,25 @@ import HandleCircle from "@/components/shared/HandleCircle"; import type { FuncMeta } from "@/types"; import { packetAtom } from "@/state/packet"; import { visualizationModeAtom } from "@/state/visualization"; -import { renderAt, renderHeatmap, renderRedundant } from "@/utils/api"; +import { + renderAt, + renderHeatmap, + renderRedundant, + renderReuseDistance, +} from "@/utils/api"; type FuncNode = Node; -function FuncCanvas({ data: { name, width, height } }: NodeProps) { +function FuncCanvas({ + data: { name, width, height, liveness_start, liveness_end }, +}: NodeProps) { const canvasRef = React.useRef(null); const globalIndex = useAtomValue(packetAtom); const visualizationMode = useAtomValue(visualizationModeAtom); + const isFuncBufferLive = React.useMemo( + () => liveness_start <= globalIndex && globalIndex <= liveness_end, + [liveness_start, liveness_end, globalIndex], + ); const nodes = useNodes(); const edges = useEdges(); @@ -34,6 +47,7 @@ function FuncCanvas({ data: { name, width, height } }: NodeProps) { () => getOutgoers({ id: name }, nodes, edges).length, [name, nodes, edges], ); + const { zoom } = useViewport(); // Latest playhead position requested, and whether a render loop is draining. // Together these coalesce rapid scrub updates: while a frame is in flight, @@ -42,94 +56,86 @@ function FuncCanvas({ data: { name, width, height } }: NodeProps) { const latestIndexRef = React.useRef(globalIndex); const renderingRef = React.useRef(false); - const paint = React.useCallback(async () => { + React.useEffect(() => { + latestIndexRef.current = globalIndex; + if (renderingRef.current) { return; } - renderingRef.current = true; - try { - // Drain to the latest requested index, skipping any that arrived while a - // previous frame was rendering. - while (true) { - const target = latestIndexRef.current; - const buffer = await renderAt(name, target); + async function render() { + try { + while (true) { + const target = latestIndexRef.current; - const ctx = canvasRef.current?.getContext("2d"); + let buffer: ArrayBuffer; - if (ctx) { - const pixels = new Uint8ClampedArray(buffer); - ctx.putImageData(new ImageData(pixels, width, height), 0, 0); - } + switch (visualizationMode) { + case "True Values": + buffer = await renderAt(name, target); + break; + case "Store Frequency": + case "Load Frequency": + buffer = await renderHeatmap(name, target, visualizationMode); + break; + case "Reuse Distance": + buffer = await renderReuseDistance(name, target); + break; + case "Redundant Stores": + buffer = await renderRedundant(name, target); + break; + } - if (latestIndexRef.current === target) { - break; - } - } - } catch (err) { - console.error(`Failed to render ${name}:`, err); - } finally { - renderingRef.current = false; - } - }, [name, width, height]); + const ctx = canvasRef.current?.getContext("2d"); - React.useEffect(() => { - if (visualizationMode !== "True Values") return; - latestIndexRef.current = globalIndex; - paint(); - }, [globalIndex, visualizationMode, paint]); + if (ctx) { + const pixels = new Uint8ClampedArray(buffer); + ctx.putImageData(new ImageData(pixels, width, height), 0, 0); + } - React.useEffect(() => { - if ( - visualizationMode !== "Store Frequency" && - visualizationMode !== "Load Frequency" - ) - return; - renderHeatmap(name, globalIndex, visualizationMode) - .then((buffer) => { - const ctx = canvasRef.current?.getContext("2d"); - if (ctx) { - ctx.putImageData( - new ImageData(new Uint8ClampedArray(buffer), width, height), - 0, - 0, - ); + if (latestIndexRef.current === target) { + break; + } } - }) - .catch((err) => - console.error(`Failed to render heatmap for ${name}:`, err), - ); - }, [globalIndex, visualizationMode, name, width, height]); + } catch { + console.error( + `Failed to render ${name} at index ${latestIndexRef.current}`, + ); + } + } - React.useEffect(() => { - if (visualizationMode !== "Redundant Stores") return; - renderRedundant(name, globalIndex) - .then((buffer) => { - const ctx = canvasRef.current?.getContext("2d"); - if (ctx) { - ctx.putImageData( - new ImageData(new Uint8ClampedArray(buffer), width, height), - 0, - 0, - ); - } - }) - .catch((err) => - console.error(`Failed to render redundant for ${name}:`, err), - ); - }, [globalIndex, visualizationMode, name, width, height]); + render(); + }, [globalIndex, name, width, height, visualizationMode]); return ( -
- +
+ {name} - +
= 1, + })} + > + = 1, + })} + /> +
{incomingEdgeCount > 0 ? ( @@ -138,7 +144,8 @@ function FuncCanvas({ data: { name, width, height } }: NodeProps) { diff --git a/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx b/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx index 914bdf5307c5..885cc49ea326 100644 --- a/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx +++ b/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx @@ -10,7 +10,7 @@ function Tracer() { return (
-
+
{Object.keys(funcs).length > 0 ? ( <> diff --git a/apps/halidoscope/frontend/src/hooks/trace.ts b/apps/halidoscope/frontend/src/hooks/trace.ts index b6137cedbcc4..1cf16c0577a3 100644 --- a/apps/halidoscope/frontend/src/hooks/trace.ts +++ b/apps/halidoscope/frontend/src/hooks/trace.ts @@ -9,6 +9,7 @@ const TraceContext = React.createContext<{ globalMaxStoreCount: number; globalMaxLoadCount: number; globalMaxRedundantCount: number; + globalMaxReuseDistance: number; }>({ funcs: {}, dagEdges: {}, @@ -16,6 +17,7 @@ const TraceContext = React.createContext<{ globalMaxStoreCount: 0, globalMaxLoadCount: 0, globalMaxRedundantCount: 0, + globalMaxReuseDistance: 0, }); export const TraceContextProvider = TraceContext.Provider; diff --git a/apps/halidoscope/frontend/src/state/graph.ts b/apps/halidoscope/frontend/src/state/graph.ts new file mode 100644 index 000000000000..05f06f0d68c0 --- /dev/null +++ b/apps/halidoscope/frontend/src/state/graph.ts @@ -0,0 +1,4 @@ +import { type Edge } from "@xyflow/react"; +import { atom } from "jotai"; + +export const edgesAtom = atom([]); diff --git a/apps/halidoscope/frontend/src/state/histogram.ts b/apps/halidoscope/frontend/src/state/histogram.ts new file mode 100644 index 000000000000..c483deb34013 --- /dev/null +++ b/apps/halidoscope/frontend/src/state/histogram.ts @@ -0,0 +1,5 @@ +import { atom } from "jotai"; + +export type HistogramScale = "linear" | "log"; + +export const histogramAtom = atom("linear"); diff --git a/apps/halidoscope/frontend/src/state/visualization.ts b/apps/halidoscope/frontend/src/state/visualization.ts index f8907226a48f..c8ba66847f64 100644 --- a/apps/halidoscope/frontend/src/state/visualization.ts +++ b/apps/halidoscope/frontend/src/state/visualization.ts @@ -4,6 +4,7 @@ export type VisualizationMode = | "True Values" | "Store Frequency" | "Load Frequency" - | "Redundant Stores"; + | "Redundant Stores" + | "Reuse Distance"; export const visualizationModeAtom = atom("True Values"); diff --git a/apps/halidoscope/frontend/src/types/index.ts b/apps/halidoscope/frontend/src/types/index.ts index ba8bff065b4e..d3452cd04e35 100644 --- a/apps/halidoscope/frontend/src/types/index.ts +++ b/apps/halidoscope/frontend/src/types/index.ts @@ -20,15 +20,13 @@ export interface FuncMeta extends Record { max_store_count: number; max_load_count: number; max_redundant_count: number; - /** - * Frequency distributions of per-pixel counts, indexed by count value (index `k` holds the - * number of pixel locations with exactly `k` stores/loads/redundant stores). The `0` bin is - * included. Length is the corresponding `max_*_count + 1`; empty when the Func has no usable - * extent. Ready to render directly as a histogram. - */ + max_reuse_distance: number; store_count_histogram: number[]; load_count_histogram: number[]; redundant_count_histogram: number[]; + reuse_distance_histogram: number[]; + liveness_start: number; + liveness_end: number; } /** Top-level payload returned by `open_trace`. Mirrors the Rust `TraceMeta`. */ @@ -39,6 +37,7 @@ export interface TraceMeta { global_max_store_count: number; global_max_load_count: number; global_max_redundant_count: number; + global_max_reuse_distance: number; } export type NodeTypes = "funcCanvas"; diff --git a/apps/halidoscope/frontend/src/utils/api.ts b/apps/halidoscope/frontend/src/utils/api.ts index d3d8590a3a58..bd59c4df8175 100644 --- a/apps/halidoscope/frontend/src/utils/api.ts +++ b/apps/halidoscope/frontend/src/utils/api.ts @@ -43,7 +43,7 @@ export async function renderAt( export async function renderHeatmap( func: string, globalIndex: number, - mode: Exclude, + mode: Exclude, ): Promise { return invoke("render_heatmap", { func, globalIndex, mode }); } @@ -60,3 +60,17 @@ export async function renderRedundant( ): Promise { return invoke("render_redundant", { func, globalIndex }); } + +/** + * Render a heatmap of maximum store-to-load reuse distances for `func` up to + * `globalIndex`. Reuse distance is measured in total packets elapsed between a + * store and the next load from the same (x, y, channel). Returns a + * `width * height * 4` RGBA8 buffer; pixels with no store→load pair are black, + * positive distances map through the Inferno colormap. + */ +export async function renderReuseDistance( + func: string, + globalIndex: number, +): Promise { + return invoke("render_reuse_distance", { func, globalIndex }); +} From c55ad215d4f63bc4772934cdfbebdf0677e6cf4b Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Wed, 24 Jun 2026 14:19:12 -0700 Subject: [PATCH 13/67] fix: Remove Python backend and shift Tauri app to root directory of apps/halidoscope. --- apps/halidoscope/{frontend => }/.gitignore | 1 + .../{frontend => }/.prettierignore | 0 apps/halidoscope/{frontend => }/.prettierrc | 0 apps/halidoscope/backend/backend/__init__.py | 0 apps/halidoscope/backend/backend/main.py | 511 ------- apps/halidoscope/backend/pyproject.toml | 28 - apps/halidoscope/backend/uv.lock | 1187 ----------------- .../{frontend => }/eslint.config.mjs | 0 apps/halidoscope/frontend/README.md | 11 - apps/halidoscope/{frontend => }/index.html | 0 apps/halidoscope/{frontend => }/package.json | 0 .../halidoscope/{frontend => }/pnpm-lock.yaml | 0 .../{frontend => }/pnpm-workspace.yaml | 0 .../{frontend => }/src-tauri/.gitignore | 0 .../{frontend => }/src-tauri/Cargo.lock | 26 +- .../{frontend => }/src-tauri/Cargo.toml | 10 +- .../{frontend => }/src-tauri/build.rs | 0 .../src-tauri/capabilities/default.json | 0 .../src-tauri/icons/128x128.png | Bin .../src-tauri/icons/128x128@2x.png | Bin .../{frontend => }/src-tauri/icons/32x32.png | Bin .../src-tauri/icons/Square107x107Logo.png | Bin .../src-tauri/icons/Square142x142Logo.png | Bin .../src-tauri/icons/Square150x150Logo.png | Bin .../src-tauri/icons/Square284x284Logo.png | Bin .../src-tauri/icons/Square30x30Logo.png | Bin .../src-tauri/icons/Square310x310Logo.png | Bin .../src-tauri/icons/Square44x44Logo.png | Bin .../src-tauri/icons/Square71x71Logo.png | Bin .../src-tauri/icons/Square89x89Logo.png | Bin .../src-tauri/icons/StoreLogo.png | Bin .../{frontend => }/src-tauri/icons/icon.icns | Bin .../{frontend => }/src-tauri/icons/icon.ico | Bin .../{frontend => }/src-tauri/icons/icon.png | Bin .../{frontend => }/src-tauri/src/commands.rs | 0 .../{frontend => }/src-tauri/src/lib.rs | 0 .../{frontend => }/src-tauri/src/main.rs | 2 +- .../{frontend => }/src-tauri/src/render.rs | 0 .../{frontend => }/src-tauri/src/trace.rs | 0 .../{frontend => }/src-tauri/tauri.conf.json | 0 apps/halidoscope/{frontend => }/src/App.css | 0 apps/halidoscope/{frontend => }/src/App.tsx | 0 .../src/components/controls/ControlPanel.tsx | 0 .../src/components/controls/ControlTabs.tsx | 0 .../components/controls/funcs/FuncsPanel.tsx | 2 +- .../controls/visualizations/GraphDisplay.tsx | 0 .../controls/visualizations/Histogram.tsx | 0 .../visualizations/HistogramSelect.tsx | 0 .../controls/visualizations/PlaybackRate.tsx | 0 .../visualizations/VisualizationsPanel.tsx | 0 .../visualizations/VisualizationsSelect.tsx | 0 .../src/components/shared/Canvas.tsx | 0 .../src/components/shared/HandleCircle.tsx | 0 .../components/views/tracer/FuncCanvas.tsx | 0 .../src/components/views/tracer/Tracer.tsx | 0 .../views/tracer/TracerTimeline.tsx | 0 .../{frontend => }/src/hooks/trace.ts | 0 apps/halidoscope/{frontend => }/src/main.tsx | 0 .../{frontend => }/src/state/func.ts | 0 .../{frontend => }/src/state/graph.ts | 0 .../{frontend => }/src/state/histogram.ts | 0 .../{frontend => }/src/state/packet.ts | 0 .../{frontend => }/src/state/playback.ts | 0 .../{frontend => }/src/state/visualization.ts | 0 .../{frontend => }/src/types/index.ts | 0 .../{frontend => }/src/utils/api.ts | 0 .../{frontend => }/src/utils/constants.ts | 0 .../{frontend => }/src/utils/graph.ts | 0 .../{frontend => }/src/vite-env.d.ts | 0 apps/halidoscope/{frontend => }/tsconfig.json | 0 .../{frontend => }/tsconfig.node.json | 0 .../halidoscope/{frontend => }/vite.config.ts | 0 72 files changed, 21 insertions(+), 1757 deletions(-) rename apps/halidoscope/{frontend => }/.gitignore (96%) rename apps/halidoscope/{frontend => }/.prettierignore (100%) rename apps/halidoscope/{frontend => }/.prettierrc (100%) delete mode 100644 apps/halidoscope/backend/backend/__init__.py delete mode 100644 apps/halidoscope/backend/backend/main.py delete mode 100644 apps/halidoscope/backend/pyproject.toml delete mode 100644 apps/halidoscope/backend/uv.lock rename apps/halidoscope/{frontend => }/eslint.config.mjs (100%) delete mode 100644 apps/halidoscope/frontend/README.md rename apps/halidoscope/{frontend => }/index.html (100%) rename apps/halidoscope/{frontend => }/package.json (100%) rename apps/halidoscope/{frontend => }/pnpm-lock.yaml (100%) rename apps/halidoscope/{frontend => }/pnpm-workspace.yaml (100%) rename apps/halidoscope/{frontend => }/src-tauri/.gitignore (100%) rename apps/halidoscope/{frontend => }/src-tauri/Cargo.lock (99%) rename apps/halidoscope/{frontend => }/src-tauri/Cargo.toml (85%) rename apps/halidoscope/{frontend => }/src-tauri/build.rs (100%) rename apps/halidoscope/{frontend => }/src-tauri/capabilities/default.json (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/128x128.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/128x128@2x.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/32x32.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/Square107x107Logo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/Square142x142Logo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/Square150x150Logo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/Square284x284Logo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/Square30x30Logo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/Square310x310Logo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/Square44x44Logo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/Square71x71Logo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/Square89x89Logo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/StoreLogo.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/icon.icns (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/icon.ico (100%) rename apps/halidoscope/{frontend => }/src-tauri/icons/icon.png (100%) rename apps/halidoscope/{frontend => }/src-tauri/src/commands.rs (100%) rename apps/halidoscope/{frontend => }/src-tauri/src/lib.rs (100%) rename apps/halidoscope/{frontend => }/src-tauri/src/main.rs (85%) rename apps/halidoscope/{frontend => }/src-tauri/src/render.rs (100%) rename apps/halidoscope/{frontend => }/src-tauri/src/trace.rs (100%) rename apps/halidoscope/{frontend => }/src-tauri/tauri.conf.json (100%) rename apps/halidoscope/{frontend => }/src/App.css (100%) rename apps/halidoscope/{frontend => }/src/App.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/controls/ControlPanel.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/controls/ControlTabs.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/controls/funcs/FuncsPanel.tsx (99%) rename apps/halidoscope/{frontend => }/src/components/controls/visualizations/GraphDisplay.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/controls/visualizations/Histogram.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/controls/visualizations/HistogramSelect.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/controls/visualizations/PlaybackRate.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/controls/visualizations/VisualizationsPanel.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/controls/visualizations/VisualizationsSelect.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/shared/Canvas.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/shared/HandleCircle.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/views/tracer/FuncCanvas.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/views/tracer/Tracer.tsx (100%) rename apps/halidoscope/{frontend => }/src/components/views/tracer/TracerTimeline.tsx (100%) rename apps/halidoscope/{frontend => }/src/hooks/trace.ts (100%) rename apps/halidoscope/{frontend => }/src/main.tsx (100%) rename apps/halidoscope/{frontend => }/src/state/func.ts (100%) rename apps/halidoscope/{frontend => }/src/state/graph.ts (100%) rename apps/halidoscope/{frontend => }/src/state/histogram.ts (100%) rename apps/halidoscope/{frontend => }/src/state/packet.ts (100%) rename apps/halidoscope/{frontend => }/src/state/playback.ts (100%) rename apps/halidoscope/{frontend => }/src/state/visualization.ts (100%) rename apps/halidoscope/{frontend => }/src/types/index.ts (100%) rename apps/halidoscope/{frontend => }/src/utils/api.ts (100%) rename apps/halidoscope/{frontend => }/src/utils/constants.ts (100%) rename apps/halidoscope/{frontend => }/src/utils/graph.ts (100%) rename apps/halidoscope/{frontend => }/src/vite-env.d.ts (100%) rename apps/halidoscope/{frontend => }/tsconfig.json (100%) rename apps/halidoscope/{frontend => }/tsconfig.node.json (100%) rename apps/halidoscope/{frontend => }/vite.config.ts (100%) diff --git a/apps/halidoscope/frontend/.gitignore b/apps/halidoscope/.gitignore similarity index 96% rename from apps/halidoscope/frontend/.gitignore rename to apps/halidoscope/.gitignore index ae6e31033ab7..0107ad7ddde4 100644 --- a/apps/halidoscope/frontend/.gitignore +++ b/apps/halidoscope/.gitignore @@ -25,3 +25,4 @@ dist-ssr # Trace binaries *.hltrace +/samples/ diff --git a/apps/halidoscope/frontend/.prettierignore b/apps/halidoscope/.prettierignore similarity index 100% rename from apps/halidoscope/frontend/.prettierignore rename to apps/halidoscope/.prettierignore diff --git a/apps/halidoscope/frontend/.prettierrc b/apps/halidoscope/.prettierrc similarity index 100% rename from apps/halidoscope/frontend/.prettierrc rename to apps/halidoscope/.prettierrc diff --git a/apps/halidoscope/backend/backend/__init__.py b/apps/halidoscope/backend/backend/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/apps/halidoscope/backend/backend/main.py b/apps/halidoscope/backend/backend/main.py deleted file mode 100644 index d019eec93480..000000000000 --- a/apps/halidoscope/backend/backend/main.py +++ /dev/null @@ -1,511 +0,0 @@ -from __future__ import annotations - -import logging -import time as _time -import uuid -from typing import Any - -import numpy as np -import uvicorn -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel -from fastapi.middleware.cors import CORSMiddleware -from fastapi.websockets import WebSocket, WebSocketDisconnect -from halide import FuncStats, Trace - -log = logging.getLogger(__name__) - -app = FastAPI(title="Halidoscope Backend") - -origins = ["http://localhost:1420"] -app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -_sessions: dict[str, Any] = {} -_packets: dict[str, Any] = {} -_store_indices: dict[str, list[int]] = {} -_load_indices: dict[str, list[int]] = {} -_func_name_cache: dict[ - str, dict[str, Any | None] -] = {} # session_id -> {packet_func -> stats | None} - - -def _analyze_packets( - session_id: str, trace: Any, funcs: dict[str, FuncStats] -) -> dict[str, Any]: - # Delegate the packet scan to C++ for performance; the result maps each - # qualified func name to its per-func max store and load counts. - max_counts = trace.compute_max_load_store_counts() - - result: dict[str, Any] = {} - global_max_store = 0 - global_max_load = 0 - - for func_name, func_stats in funcs.items(): - counts = max_counts.get(func_name) - if counts is None: - continue - - max_store: int = counts["max_store_count"] - max_load: int = counts["max_load_count"] - - min_coords = list(func_stats.min_coords) - max_coords = list(func_stats.max_coords) - width = max_coords[0] - min_coords[0] - height = ( - max_coords[1] - min_coords[1] - if len(min_coords) > 1 and len(max_coords) > 1 - else 1 - ) - - entry: dict[str, Any] = { - "name": func_name, - "width": width, - "height": height, - "min_coords": min_coords, - "max_coords": max_coords, - "min_value": func_stats.min_value, - "max_value": func_stats.max_value, - "max_store_count": max_store, - "max_load_count": max_load, - } - result[func_name] = entry - _func_name_cache[session_id][func_name] = entry - - global_max_store = max(global_max_store, max_store) - global_max_load = max(global_max_load, max_load) - - return result, global_max_store, global_max_load - - -def _register_trace(trace: Any) -> dict[str, Any]: - session_id = str(uuid.uuid4()) - - # Cache once; avoids full C++ vector copy on each access. - _packets[session_id] = trace.packets - _store_indices[session_id] = np.array(trace.store_indices()) - _load_indices[session_id] = np.array(trace.load_indices()) - _func_name_cache[session_id] = {} - - # Analyze packets to compute load/store counts and other stats per Func. - funcs, global_max_store, global_max_load = _analyze_packets( - session_id, trace, trace.funcs - ) - - payload = { - "session_id": session_id, - "num_packets": len(trace), - "funcs": funcs, - "dag_edges": {k: list(v) for k, v in trace.dag_edges.items()}, - "pipelines": {str(k): v for k, v in trace.pipelines.items()}, - "global_max_store_count": global_max_store, - "global_max_load_count": global_max_load, - } - _sessions[session_id] = payload - - return payload - - -class LoadPathRequest(BaseModel): - path: str - - -@app.post("/load-path") -async def load_trace_path(request: LoadPathRequest) -> dict[str, Any]: - try: - with open(request.path, "rb") as f: - data = f.read() - except OSError as e: - raise HTTPException(status_code=400, detail=str(e)) - - trace = Trace.load_bytes(data) - return _register_trace(trace) - - -def _get_func_item_for_packet(session_id: str, func_name: str) -> Any: - cache = _func_name_cache[session_id] - if func_name in cache: - return cache[func_name] - - funcs = _sessions[session_id]["funcs"] - for name, stats in funcs.items(): - if name == func_name or name.endswith(f":{func_name}"): - cache[func_name] = stats - return stats - - cache[func_name] = None - return None - - -def _render_range(session_id: str, start: int, end: int) -> list[dict[str, Any]]: - packets = _packets[session_id] - indices = _store_indices[session_id] - - # pending: func_name -> [px_list, py_list, c_list, val_list, func_stats] - pending: dict[str, list] = {} - - end = min(end, len(packets)) - - lo = np.searchsorted(indices, start, side="left") - hi = np.searchsorted(indices, end - 1, side="right") - for si in range(lo, hi): - # Grab the next load or store packet in the requested range. - packet = packets[indices[si]] - - func_stats = _get_func_item_for_packet(session_id, packet.func) - coords = np.asarray(packet.coordinates) - values_arr = np.asarray(packet.get_values()) - n_lanes = packet.type_lanes - dims_per_lane = len(coords) // n_lanes - min_coords = func_stats["min_coords"] - min_x = min_coords[0] if min_coords else 0 - min_y = min_coords[1] if len(min_coords) > 1 else 0 - n = min(n_lanes, len(values_arr)) - - # Check to see if there are pending updates for this Func; if not, - # initialize the lists and cache the func_stats. - if func_stats["name"] not in pending: - pending[func_stats["name"]] = [[], [], [], [], func_stats] - px_list, py_list, c_list, val_list, _ = pending[func_stats["name"]] - - xs = coords[:n_lanes] - min_x - ys = ( - coords[n_lanes : 2 * n_lanes] - min_y - if dims_per_lane >= 2 - else np.full(n_lanes, -min_y, dtype=np.intp) - ) - cs = ( - coords[2 * n_lanes : 3 * n_lanes] - if dims_per_lane >= 3 - else np.full(n_lanes, -1, dtype=np.intp) - ) - - px_list.extend(xs[:n].tolist()) - py_list.extend(ys[:n].tolist()) - c_list.extend(cs[:n].tolist()) - val_list.extend(values_arr[:n].tolist()) - - updates = [] - for func_name, (px_list, py_list, c_list, val_list, func_stats) in pending.items(): - if not px_list: - continue - - xs = np.asarray(px_list, dtype=np.intp) - ys = np.asarray(py_list, dtype=np.intp) - vals = np.asarray(val_list) - - min_v = func_stats["min_value"] or 0.0 - max_v = func_stats["max_value"] or 255.0 - if max_v > min_v: - normalized = np.clip( - (255.0 * (vals - min_v) / (max_v - min_v)), 0, 255 - ).astype(np.uint8) - else: - normalized = np.full(len(xs), 128, dtype=np.uint8) - - min_coords = func_stats["min_coords"] - max_coords = func_stats["max_coords"] - width = max_coords[0] - min_coords[0] - height = ( - max(1, max_coords[1] - min_coords[1]) - if len(min_coords) > 1 and len(max_coords) > 1 - else 1 - ) - - mask = (xs >= 0) & (xs < width) & (ys >= 0) & (ys < height) - xs = xs[mask] - ys = ys[mask] - normalized = normalized[mask] - - is_color = ( - len(min_coords) >= 3 - and len(max_coords) >= 3 - and max_coords[2] - min_coords[2] >= 3 - ) - - if is_color: - cs = np.asarray(c_list, dtype=np.intp)[mask] - update: dict[str, Any] = {"func": func_name} - for ch_idx, key in [(0, "r"), (1, "g"), (2, "b")]: - m = cs == ch_idx - if m.any(): - update[key] = { - "xs": xs[m].tolist(), - "ys": ys[m].tolist(), - "values": normalized[m].tolist(), - } - if len(update) > 1: - updates.append(update) - elif len(xs): - updates.append( - { - "func": func_name, - "xs": xs.tolist(), - "ys": ys.tolist(), - "values": normalized.tolist(), - } - ) - - return updates - - -@app.websocket("/ws/{session_id}") -async def render_ws(websocket: WebSocket, session_id: str) -> None: - await websocket.accept() - - try: - if session_id not in _sessions: - await websocket.close(code=4004, reason="session not found") - return - - log.info("ws connected: session=%s", session_id) - - while True: - msg = await websocket.receive_json() - start: int = msg["start"] - end: int = msg["end"] - log.info("ws range request: start=%d end=%d", start, end) - - t0 = _time.perf_counter() - updates = _render_range(session_id, start, end) - t1 = _time.perf_counter() - - await websocket.send_json( - {"updates": updates, "done": True, "start": start, "end": end} - ) - t2 = _time.perf_counter() - - log.info( - "render=%dms send=%dms total=%dms funcs=%d start=%d end=%d", - 1000 * (t1 - t0), - 1000 * (t2 - t1), - 1000 * (t2 - t0), - len(updates), - start, - end, - ) - - except WebSocketDisconnect: - pass - except Exception: - log.exception("WebSocket error for session %s", session_id) - await websocket.close(code=1011, reason="internal error") - - -def _track_stores(session_id: str, start: int, end: int) -> list[dict[str, Any]]: - packets = _packets[session_id] - store_indices = _store_indices[session_id] - - end = min(end, len(packets)) - lo = np.searchsorted(store_indices, start, side="left") - hi = np.searchsorted(store_indices, end - 1, side="right") - - # func_name -> [xs, ys, func_stats] - pending: dict[str, list] = {} - - for store_i in range(lo, hi): - packet = packets[store_indices[store_i]] - func_stats = _get_func_item_for_packet(session_id, packet.func) - if func_stats is None: - continue - - func_name = func_stats["name"] - if func_name not in pending: - pending[func_name] = [[], [], func_stats] - xs_list, ys_list, _ = pending[func_name] - - min_coords = func_stats["min_coords"] - max_coords = func_stats["max_coords"] - min_x = min_coords[0] if min_coords else 0 - min_y = min_coords[1] if len(min_coords) > 1 else 0 - width = max_coords[0] - min_coords[0] - height = ( - max(1, max_coords[1] - min_coords[1]) - if len(min_coords) > 1 and len(max_coords) > 1 - else 1 - ) - - coords = np.asarray(packet.coordinates) - n_lanes = packet.type_lanes - dims_per_lane = len(coords) // n_lanes - - xs = coords[:n_lanes] - min_x - ys = ( - coords[n_lanes : 2 * n_lanes] - min_y - if dims_per_lane >= 2 - else np.full(n_lanes, -min_y, dtype=np.intp) - ) - mask = (xs >= 0) & (xs < width) & (ys >= 0) & (ys < height) - xs_list.extend(xs[mask].tolist()) - ys_list.extend(ys[mask].tolist()) - - return [ - {"func": func_name, "xs": xs_list, "ys": ys_list} - for func_name, (xs_list, ys_list, _) in pending.items() - if xs_list - ] - - -def _track_loads(session_id: str, start: int, end: int) -> list[dict[str, Any]]: - packets = _packets[session_id] - load_indices = _load_indices[session_id] - - end = min(end, len(packets)) - lo = np.searchsorted(load_indices, start, side="left") - hi = np.searchsorted(load_indices, end - 1, side="right") - - # func_name -> [xs, ys, func_stats] - pending: dict[str, list] = {} - - for load_i in range(lo, hi): - packet = packets[load_indices[load_i]] - func_stats = _get_func_item_for_packet(session_id, packet.func) - if func_stats is None: - continue - - func_name = func_stats["name"] - if func_name not in pending: - pending[func_name] = [[], [], func_stats] - xs_list, ys_list, _ = pending[func_name] - - min_coords = func_stats["min_coords"] - max_coords = func_stats["max_coords"] - min_x = min_coords[0] if min_coords else 0 - min_y = min_coords[1] if len(min_coords) > 1 else 0 - width = max_coords[0] - min_coords[0] - height = ( - max(1, max_coords[1] - min_coords[1]) - if len(min_coords) > 1 and len(max_coords) > 1 - else 1 - ) - - coords = np.asarray(packet.coordinates) - n_lanes = packet.type_lanes - dims_per_lane = len(coords) // n_lanes - - xs = coords[:n_lanes] - min_x - ys = ( - coords[n_lanes : 2 * n_lanes] - min_y - if dims_per_lane >= 2 - else np.full(n_lanes, -min_y, dtype=np.intp) - ) - mask = (xs >= 0) & (xs < width) & (ys >= 0) & (ys < height) - xs_list.extend(xs[mask].tolist()) - ys_list.extend(ys[mask].tolist()) - - return [ - {"func": func_name, "xs": xs_list, "ys": ys_list} - for func_name, (xs_list, ys_list, _) in pending.items() - if xs_list - ] - - -@app.websocket("/ws/{session_id}/loads") -async def render_loads_ws(websocket: WebSocket, session_id: str) -> None: - await websocket.accept() - - try: - if session_id not in _sessions: - await websocket.close(code=4004, reason="session not found") - return - - log.info("ws connected: session=%s", session_id) - - while True: - msg = await websocket.receive_json() - start: int = msg["start"] - end: int = msg["end"] - log.info("ws loads range request: start=%d end=%d", start, end) - - t0 = _time.perf_counter() - updates = _track_loads(session_id, start, end) - t1 = _time.perf_counter() - - await websocket.send_json( - {"updates": updates, "done": True, "start": start, "end": end} - ) - t2 = _time.perf_counter() - - log.info( - "track_loads render=%dms send=%dms total=%dms funcs=%d start=%d end=%d", - 1000 * (t1 - t0), - 1000 * (t2 - t1), - 1000 * (t2 - t0), - len(updates), - start, - end, - ) - - except WebSocketDisconnect: - pass - except Exception: - log.exception("WebSocket error for session %s", session_id) - await websocket.close(code=1011, reason="internal error") - - -@app.websocket("/ws/{session_id}/stores") -async def render_stores_ws(websocket: WebSocket, session_id: str) -> None: - await websocket.accept() - - try: - if session_id not in _sessions: - await websocket.close(code=4004, reason="session not found") - return - - log.info("ws connected: session=%s", session_id) - - while True: - msg = await websocket.receive_json() - start: int = msg["start"] - end: int = msg["end"] - log.info("ws stores range request: start=%d end=%d", start, end) - - t0 = _time.perf_counter() - updates = _track_stores(session_id, start, end) - t1 = _time.perf_counter() - - await websocket.send_json( - {"updates": updates, "done": True, "start": start, "end": end} - ) - t2 = _time.perf_counter() - - log.info( - "track_stores render=%dms send=%dms total=%dms funcs=%d start=%d end=%d", - 1000 * (t1 - t0), - 1000 * (t2 - t1), - 1000 * (t2 - t0), - len(updates), - start, - end, - ) - - except WebSocketDisconnect: - pass - except Exception: - log.exception("WebSocket error for session %s", session_id) - await websocket.close(code=1011, reason="internal error") - - -@app.delete("/session/{session_id}") -async def delete_session(session_id: str) -> dict[str, str]: - if session_id not in _sessions: - raise HTTPException(status_code=404, detail="session not found") - del _sessions[session_id] - del _packets[session_id] - del _store_indices[session_id] - del _load_indices[session_id] - del _func_name_cache[session_id] - - return {"deleted": session_id} - - -def run() -> None: - logging.basicConfig(level=logging.INFO) - uvicorn.run( - "backend.main:app", host="127.0.0.1", port=8765, reload=False, log_level="info" - ) diff --git a/apps/halidoscope/backend/pyproject.toml b/apps/halidoscope/backend/pyproject.toml deleted file mode 100644 index 4907fce8dcf1..000000000000 --- a/apps/halidoscope/backend/pyproject.toml +++ /dev/null @@ -1,28 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "halidoscope" -version = "0.1.0" -description = "FastAPI backend for Halidoscope" -requires-python = ">=3.13" -dependencies = ["fastapi[standard]>=0.115", "halide", "numpy>=2.4.5"] - -[project.scripts] -dev = "backend.main:run" - -[tool.uv.sources] -halide = { path = "../../.." } -numpy = { index = "piwheels", marker = "platform_machine == 'armv8l' or platform_machine == 'armv7l'" } - -[[tool.uv.index]] -name = "piwheels" -url = "https://piwheels.org/simple" -explicit = true - -[tool.hatch.build.targets.wheel] -packages = ["backend"] - -[dependency-groups] -dev = ["ruff>=0.14"] diff --git a/apps/halidoscope/backend/uv.lock b/apps/halidoscope/backend/uv.lock deleted file mode 100644 index fbb2e7acca75..000000000000 --- a/apps/halidoscope/backend/uv.lock +++ /dev/null @@ -1,1187 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.13" -resolution-markers = [ - "(python_full_version >= '3.14' and platform_machine == 'armv7l') or (python_full_version >= '3.14' and platform_machine == 'armv8l')", - "(python_full_version < '3.14' and platform_machine == 'armv7l') or (python_full_version < '3.14' and platform_machine == 'armv8l')", - "python_full_version >= '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", - "python_full_version < '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", -] - -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, -] - -[[package]] -name = "certifi" -version = "2026.5.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, -] - -[[package]] -name = "click" -version = "8.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "detect-installer" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, -] - -[[package]] -name = "dnspython" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, -] - -[[package]] -name = "email-validator" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, -] - -[[package]] -name = "fastapi" -version = "0.136.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "email-validator" }, - { name = "fastapi-cli", extra = ["standard"] }, - { name = "fastar" }, - { name = "httpx" }, - { name = "jinja2" }, - { name = "pydantic-extra-types" }, - { name = "pydantic-settings" }, - { name = "python-multipart" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[[package]] -name = "fastapi-cli" -version = "0.0.24" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rich-toolkit" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "fastapi-cloud-cli" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[[package]] -name = "fastapi-cloud-cli" -version = "0.19.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "detect-installer" }, - { name = "fastar" }, - { name = "httpx" }, - { name = "pydantic", extra = ["email"] }, - { name = "rich-toolkit" }, - { name = "rignore" }, - { name = "sentry-sdk" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/7c/f194925af8fabdb0b7a886a1b89087c0b7f327f99e79497a882aa94c1e34/fastapi_cloud_cli-0.19.0.tar.gz", hash = "sha256:f97b31c2ad6af3832eb4065870bdca3365b6e827a0ccf6eeb15e477bc1662b13", size = 57476, upload-time = "2026-06-01T08:24:03.407Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/e6/1a2ec890fc273b9da2b173ca45f692a2e24a369bdd39ea7812c1d8a799e5/fastapi_cloud_cli-0.19.0-py3-none-any.whl", hash = "sha256:a2dfc4074c321e63ec88589cc1f90573d4b5bf980ddc44a7033e6f3cd8e96628", size = 38239, upload-time = "2026-06-01T08:24:02.437Z" }, -] - -[[package]] -name = "fastar" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/d6/3be260037e86fb694e88d47f583bac3a0188c99cee1a6b257ac26cb6b53c/fastar-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:33f544b08b4541b678e53749b4552a44720d96761fb79c172b005b1089c443ed", size = 707975, upload-time = "2026-04-13T17:09:58.866Z" }, - { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2b/d11d84bdd5e0e377771b955755771e3460b290da5809cb78c1b735ee2228/fastar-0.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:881247e6b6eaea59fc6569f9b61447aa6b9fc2ee864e048b4643d69c52745805", size = 863054, upload-time = "2026-04-13T17:09:13.048Z" }, - { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, - { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, - { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, - { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, - { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, - { url = "https://files.pythonhosted.org/packages/b7/9b/fa42ea1188b144bac4b1b60753dfd449974a4d5eda132029ee7711569f94/fastar-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e8b993cb5613bab495ed482810bedc0986633fcb9a3b55c37ec88e0d6714f6a", size = 1071147, upload-time = "2026-04-13T17:10:48.833Z" }, - { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, - { url = "https://files.pythonhosted.org/packages/db/33/5f11f23eca0a569cd052507bc45dda2e5468697f8665728d25be44120f7d/fastar-0.11.0-cp313-cp313-win32.whl", hash = "sha256:c5f63d4d99ff4bfb37c659982ec413358bdee747005348756cc50a04d412d989", size = 454089, upload-time = "2026-04-13T17:11:46.821Z" }, - { url = "https://files.pythonhosted.org/packages/da/2f/35ff03c939cba7a255a9132367873fec6c355fd06a7f84fedcbaf4c8129f/fastar-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8690ed1928d31ded3ada308e1086525fb3871f5fa81e1b69601a3f7774004583", size = 486312, upload-time = "2026-04-13T17:11:32.86Z" }, - { url = "https://files.pythonhosted.org/packages/ef/71/ee9246cbfcbfd4144558f35e7e9a306ffe0a7564730a5188c45f21d2dab8/fastar-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:d977ded9d98a0719a305e0a4d5ee811f1d3e856d853a50acb8ae833c3cd6d5d2", size = 461975, upload-time = "2026-04-13T17:11:22.589Z" }, - { url = "https://files.pythonhosted.org/packages/7a/cd/3644c48ecac456f928c12d47ec3bed36c36555b17c3859856f1ff860265d/fastar-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:71375bd6f03c2a43eb47bd949ea38ff45434917f9cdac79675c5b9f60de4fa73", size = 707860, upload-time = "2026-04-13T17:10:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/69/ca/dee04476ae3626b2b040a60ad84628f77e1ffd8444232f2426b0ca1e0d7e/fastar-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eddfd9cab16e19ae247fe44bf992cb403ccfe27d3931d6de29a4695d95ad386c", size = 628216, upload-time = "2026-04-13T17:09:45.355Z" }, - { url = "https://files.pythonhosted.org/packages/dc/5e/9395c7353d079cb4f5be0f7982ce0dc9f2e7dec5fd175eef466729d6023a/fastar-0.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c371f1d4386c699018bb64eb2fa785feacf32785559049d2bb72fe4af023f53", size = 864378, upload-time = "2026-04-13T17:09:14.611Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/1e4f67148223ff219612b6281a6000357abbcc2417964fa5c83f11d68fce/fastar-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cad7fa41e3e66554387481c1a09365e4638becd322904932674159d5f4046728", size = 760921, upload-time = "2026-04-13T17:07:59.138Z" }, - { url = "https://files.pythonhosted.org/packages/0f/82/09d11fb6d12f17993ffaf32ffd30c3c121a11e2966e84f19fb6f66430118/fastar-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf36652fa71b83761717c9899b98732498f8a2cb6327ff16bbf07f6be85c3437", size = 757012, upload-time = "2026-04-13T17:08:14.186Z" }, - { url = "https://files.pythonhosted.org/packages/52/1f/5aeeacc4cb65615e2c9292cd9c5b0cd6fb6d2e6ee472ca6adc6c1b1b22ef/fastar-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f68ff8c17833053da4841720e95edde80ce45bb994b6b7d51418dddaac70ee47", size = 924510, upload-time = "2026-04-13T17:08:28.741Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1a/1e5bdabbeaf2e856928956292609f2ff6a650f94480fb8afaca30229e483/fastar-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4563ed37a12ea1cdc398af8571258d24b988bf342b7b3bf5451bd5891243280c", size = 816602, upload-time = "2026-04-13T17:08:59.461Z" }, - { url = "https://files.pythonhosted.org/packages/87/24/f960147910da3bed41a3adfcb026e17d5f50f4cf467a3324237a7088f61a/fastar-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee63c9875cba3b70dc44338c560facc5d6e763047dcc4a30501f9a68cf5f890", size = 819452, upload-time = "2026-04-13T17:09:29.926Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f4/3e77d7901d5707fd7f8a352e153c8ae09ea974e6fabad0b7c4eb9944b8d4/fastar-0.11.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:bd76bfffae6d0a91f4ac4a612f721e7aec108db97dccdd120ae063cd66959f27", size = 885254, upload-time = "2026-04-13T17:08:44.285Z" }, - { url = "https://files.pythonhosted.org/packages/47/01/1585edd5ec47782ae93cd94edf05828e0ab02ef00aec00aea4194a600464/fastar-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5b707501ec01c1bc0518f741f01d322e50c9adc19a451aa24f67a2316e9397", size = 971496, upload-time = "2026-04-13T17:10:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e9/6874c9d1236ded565a0bed54b320ac9f165f287b1d89490fb70f9f323c81/fastar-0.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:37c0b5a88a657839aad98b0a6c9e4ac4c2c15d6b49c44ee3935c6b08e9d3e479", size = 1034685, upload-time = "2026-04-13T17:10:34.063Z" }, - { url = "https://files.pythonhosted.org/packages/14/d8/4ab20613ce2983427aee958e39be878dba874aa227c530a845e32429c4f6/fastar-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6c55f536c62a6efb180c1af0d5182948bff576bbfe6276e8e1359c9c7d2215d8", size = 1072675, upload-time = "2026-04-13T17:10:50.53Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ae/5ac3b7c20ce4b08f011dd2b979f96caabe64f9b10b157f211ea91bdfadca/fastar-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3082eeca59e189b9039335862f4c2780c0c8871d656bfdf559db4414a105b251", size = 1029330, upload-time = "2026-04-13T17:11:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e7/37cd6a1d4e288292170b64e19d79ecce2a7de8bb76790323399a2abc4619/fastar-0.11.0-cp314-cp314-win32.whl", hash = "sha256:b201a0a4e29f9fec2a177e13154b8725ec65ab9f83bd6415483efaa2aa18344b", size = 453940, upload-time = "2026-04-13T17:11:48.713Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1c/795c878b1ee29d79021cf8ed81f18f2b25ccde58453b0d34b9bdc7e025ea/fastar-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:868fddb26072a43e870a8819134b9f80ee602931be5a76e6fb873e04da343637", size = 486334, upload-time = "2026-04-13T17:11:34.882Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a4/113f104301df8bddcc0b3775b611a30cb7610baa3add933c7ccac9386467/fastar-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:3db39c9cc42abb0c780a26b299f24dfbc8be455985e969e15336d70d7b2f833b", size = 461534, upload-time = "2026-04-13T17:11:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a6/5c5f2c2c8e0c63e56a5636ebc7721589c889e94c0092cec7eb28ae7207e6/fastar-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:49c3299dec5e125e7ebaa27545714da9c7391777366015427e0ae62d548b442b", size = 707156, upload-time = "2026-04-13T17:10:02.176Z" }, - { url = "https://files.pythonhosted.org/packages/df/f7/982c01b61f0fc135ad2b16d01e6d0ee53cf8791e68827f5f7c5a65b2e5b1/fastar-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3328ed1ed56d31f5198350b17dd60449b8d6b9d47abb4688bab6aef4450a165b", size = 627032, upload-time = "2026-04-13T17:09:46.978Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c3/38f1dac77ae0c71c37b176277c96d830796b8ce2fe69705f917829b53829/fastar-0.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd3eca3bbfec84a614bcb4143b4ad4f784d0895babc26cfc88436af88ca23c7a", size = 864403, upload-time = "2026-04-13T17:09:16.58Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f0/e69c363bdb3e5a5848e937b662b5469581ee6682c51bc1c0556494773929/fastar-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff86a967acb0d621dd24063dda090daa67bf4993b9570e97fe156de88a9006ca", size = 759480, upload-time = "2026-04-13T17:08:00.599Z" }, - { url = "https://files.pythonhosted.org/packages/3b/29/4d8737590c2a6357d614d7cc7288e8f68e7e449680b8922997cc4349e65e/fastar-0.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86eaf7c0e985d93a7734168be2fb232b2a8cca53e41431c2782d7c12b12c03b1", size = 756219, upload-time = "2026-04-13T17:08:15.699Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ec/400de7b3b7d48801908f19cf5462177104395799472671b3e8152b2b04ca/fastar-0.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91f07b0b8eb67e2f177733a1f884edad7dfb9f8977ffef15927b20cb9604027d", size = 923669, upload-time = "2026-04-13T17:08:30.574Z" }, - { url = "https://files.pythonhosted.org/packages/5d/01/8926c53da923fed7ab4b96e7fbf7f73b663beb4f02095b654d6fab46f9ad/fastar-0.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f85c896885eb4abf1a635d54dea22cac6ae48d04fc2ea26ae652fcf1febe1220", size = 815729, upload-time = "2026-04-13T17:09:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/89/f0/5fef4c7946e352651b504b1a4235dac3505e7cfd24020788ab50552e84bf/fastar-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:075c07095c8de4b774ba8f28b9c0a02b1a2cd254da50cbe464dd3bb2432e9158", size = 819812, upload-time = "2026-04-13T17:09:31.907Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c8/0ebc3298b4a45e7bddc50b169ae6a6f5b80c939394d4befe6e60de535ee7/fastar-0.11.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:07f028933820c65750baf3383b807ecce1cd9385cf00ce192b79d263ad6b856c", size = 884074, upload-time = "2026-04-13T17:08:45.802Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9f/7baa4cdff8d6fbca41fa5c764b48a941fed8a9ec6c4cc92de65895a28299/fastar-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:039f875efa0f01fa43c20bf4e2fc7305489c61d0ac76eda991acfba7820a0e63", size = 969450, upload-time = "2026-04-13T17:10:18.667Z" }, - { url = "https://files.pythonhosted.org/packages/d4/dc/1ebbfb58a47056ba866494f19efbcdd2ba2897096b94f36e796594b4d05b/fastar-0.11.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fff12452a9a5c6814a012445f26365541cc3d99dcca61f09762e6a389f7a32ea", size = 1033775, upload-time = "2026-04-13T17:10:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/c2/5f/ce4e3914066f08c99eb8c32952cc07c1a013e81b1db1b0f598130bf6b974/fastar-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2bf733e09f942b6fa876efe30a90508d1f4caef5630c00fb2a84fba355873712", size = 1072158, upload-time = "2026-04-13T17:10:52.497Z" }, - { url = "https://files.pythonhosted.org/packages/03/2a/6bca72992c84151c387cc6558f3867f5ebe5fb3684ee6fa9b76280ba4b8e/fastar-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d1531fa848fdd3677d2dce0a4b436ea64d9ae38fb8babe2ddbc180dd153cb7a3", size = 1028577, upload-time = "2026-04-13T17:11:09.934Z" }, - { url = "https://files.pythonhosted.org/packages/83/18/7a7c15657a3da5569b26fc51cde6a80f8d84cb54b3b1aea6d74a103db4ad/fastar-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:5744551bc67c6fc6581cbd0e34a0fd6e2cd0bd30b43e94b1c3119cf35064b162", size = 453601, upload-time = "2026-04-13T17:11:53.726Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d8/331b59a6de279f3ad75c10c02c40a12f21d64a437d9c3d6f1af2dcbd7a76/fastar-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f4ce44e3b56c47cf38244b98d29f269b259740a580c47a2552efa5b96a5458fb", size = 486436, upload-time = "2026-04-13T17:11:40.089Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fd/5390ec4f49100f3ecb9968a392f9e6d039f1e3fe0ecd28443716ff01e589/fastar-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:76c1359314355eafbc6989f20fb1ad565a3d10200117923b9da765a17e2f6f11", size = 461049, upload-time = "2026-04-13T17:11:25.918Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "halide" -source = { directory = "../../../" } -dependencies = [ - { name = "imageio", version = "2.37.3", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, - { name = "imageio", version = "2.37.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, - { name = "numpy", version = "2.4.5", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, - { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, - { name = "pillow", version = "12.2.0", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, -] - -[package.metadata] -requires-dist = [ - { name = "imageio", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=2" }, - { name = "imageio", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=2", index = "https://piwheels.org/simple" }, - { name = "numpy", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=1.26" }, - { name = "numpy", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=1.26", index = "https://piwheels.org/simple" }, - { name = "pillow", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", index = "https://piwheels.org/simple" }, -] - -[package.metadata.requires-dev] -apps = [ - { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, - { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, - { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, -] -ci-base = [ - { name = "cmake", specifier = ">=3.28" }, - { name = "ninja", specifier = ">=1.11,!=1.13.0" }, - { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, - { name = "pre-commit", specifier = ">=4" }, - { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, - { name = "pybind11", specifier = ">=2.11.1" }, - { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, - { name = "ruff", specifier = ">=0.12" }, - { name = "scikit-build-core", specifier = "~=0.11.0" }, - { name = "setuptools-scm", specifier = ">=8.3.1" }, - { name = "tbump", specifier = ">=6.11" }, -] -ci-llvm-21 = [ - { name = "cmake", specifier = ">=3.28" }, - { name = "halide-llvm", specifier = "~=21.1.0", index = "https://pypi.halide-lang.org/simple" }, - { name = "ninja", specifier = ">=1.11,!=1.13.0" }, - { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, - { name = "pre-commit", specifier = ">=4" }, - { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, - { name = "pybind11", specifier = ">=2.11.1" }, - { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, - { name = "ruff", specifier = ">=0.12" }, - { name = "scikit-build-core", specifier = "~=0.11.0" }, - { name = "setuptools-scm", specifier = ">=8.3.1" }, - { name = "tbump", specifier = ">=6.11" }, -] -ci-llvm-22 = [ - { name = "cmake", specifier = ">=3.28" }, - { name = "halide-llvm", specifier = "~=22.1.0", index = "https://pypi.halide-lang.org/simple" }, - { name = "ninja", specifier = ">=1.11,!=1.13.0" }, - { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, - { name = "pre-commit", specifier = ">=4" }, - { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, - { name = "pybind11", specifier = ">=2.11.1" }, - { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, - { name = "ruff", specifier = ">=0.12" }, - { name = "scikit-build-core", specifier = "~=0.11.0" }, - { name = "setuptools-scm", specifier = ">=8.3.1" }, - { name = "tbump", specifier = ">=6.11" }, -] -ci-llvm-main = [ - { name = "cmake", specifier = ">=3.28" }, - { name = "halide-llvm", specifier = "~=23.0.0.dev0", index = "https://pypi.halide-lang.org/simple" }, - { name = "ninja", specifier = ">=1.11,!=1.13.0" }, - { name = "onnx", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = "==1.19.0" }, - { name = "pre-commit", specifier = ">=4" }, - { name = "protobuf", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=7" }, - { name = "pybind11", specifier = ">=2.11.1" }, - { name = "pytest", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, - { name = "ruff", specifier = ">=0.12" }, - { name = "scikit-build-core", specifier = "~=0.11.0" }, - { name = "setuptools-scm", specifier = ">=8.3.1" }, - { name = "tbump", specifier = ">=6.11" }, -] -dev = [ - { name = "pybind11", specifier = ">=2.11.1" }, - { name = "scikit-build-core", specifier = "~=0.11.0" }, - { name = "setuptools-scm", specifier = ">=8.3.1" }, -] -tools = [ - { name = "cmake", specifier = ">=3.28" }, - { name = "ninja", specifier = ">=1.11,!=1.13.0" }, - { name = "pre-commit", specifier = ">=4" }, - { name = "ruff", specifier = ">=0.12" }, - { name = "tbump", specifier = ">=6.11" }, -] - -[[package]] -name = "halidoscope" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "fastapi", extra = ["standard"] }, - { name = "halide" }, - { name = "numpy", version = "2.4.5", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, - { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, -] - -[package.dev-dependencies] -dev = [ - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "fastapi", extras = ["standard"], specifier = ">=0.115" }, - { name = "halide", directory = "../../../" }, - { name = "numpy", marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'", specifier = ">=2.4.5" }, - { name = "numpy", marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'", specifier = ">=2.4.5", index = "https://piwheels.org/simple" }, -] - -[package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.14" }] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httptools" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, - { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, - { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, - { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, - { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, - { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, - { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, - { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, - { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, - { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, - { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "idna" -version = "3.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, -] - -[[package]] -name = "imageio" -version = "2.37.3" -source = { registry = "https://piwheels.org/simple" } -resolution-markers = [ - "(python_full_version >= '3.14' and platform_machine == 'armv7l') or (python_full_version >= '3.14' and platform_machine == 'armv8l')", - "(python_full_version < '3.14' and platform_machine == 'armv7l') or (python_full_version < '3.14' and platform_machine == 'armv8l')", -] -dependencies = [ - { name = "numpy", version = "2.4.5", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, - { name = "pillow", version = "12.2.0", source = { registry = "https://piwheels.org/simple" }, marker = "platform_machine == 'armv7l' or platform_machine == 'armv8l'" }, -] -wheels = [ - { url = "https://piwheels.org/simple/imageio/imageio-2.37.3-py3-none-any.whl", hash = "sha256:06c1f430a489e305a69e006b6877451fdffe2c506099e9792d94fae3bb69cd7a" }, -] - -[[package]] -name = "imageio" -version = "2.37.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", - "python_full_version < '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", -] -dependencies = [ - { name = "numpy", version = "2.4.5", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, - { name = "pillow", version = "12.2.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'armv7l' and platform_machine != 'armv8l'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "numpy" -version = "2.4.5" -source = { registry = "https://piwheels.org/simple" } -resolution-markers = [ - "(python_full_version >= '3.14' and platform_machine == 'armv7l') or (python_full_version >= '3.14' and platform_machine == 'armv8l')", - "(python_full_version < '3.14' and platform_machine == 'armv7l') or (python_full_version < '3.14' and platform_machine == 'armv8l')", -] -wheels = [ - { url = "https://piwheels.org/simple/numpy/numpy-2.4.5-cp313-cp313-linux_armv7l.whl", hash = "sha256:b2fcc5c1c99207339ce3c69cab6a7714f8923d3953b4e892cb061610c36bf669" }, -] - -[[package]] -name = "numpy" -version = "2.4.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", - "python_full_version < '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", -] -sdist = { url = "https://files.pythonhosted.org/packages/50/8e/b8041bc719f056afd864478029d52214789341ac6583437b0ee5031e9530/numpy-2.4.5.tar.gz", hash = "sha256:ca670567a5683b7c1670ec03e0ddd5862e10934e92a70751d68d7b7b74ca7f9f", size = 20735669, upload-time = "2026-05-15T20:25:19.492Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/a4/fb50657c7cab297bf34edcd60a074cb0647f61771430d6363575274160fe/numpy-2.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ef248460b645c102026b82337cc4e88231909c66dd77b59ec6d6cac7e44f277", size = 16684760, upload-time = "2026-05-15T20:23:19.436Z" }, - { url = "https://files.pythonhosted.org/packages/3e/43/87e731299b9408eda705b3b9cb31c7bceb9347d2af9cbb16b2b1e4b5bc0f/numpy-2.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4603622bdcdbf8dccb1d9d5b21d16a7aa4e473ae6c8e14048d846fd4ca2907a0", size = 14694117, upload-time = "2026-05-15T20:23:21.832Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/0b2bb8acea222e9dd6e582afc2bc553b89b8833cbdccc68e68f050fb31f8/numpy-2.4.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6c18d49c67689c562854b53fdc433b93e47c12952aa6fa6d59f185e1a5992419", size = 5199141, upload-time = "2026-05-15T20:23:24.066Z" }, - { url = "https://files.pythonhosted.org/packages/39/60/b6972b5d47033d90000f0097c81a98b9486589a2d7003bf725bff275cb0d/numpy-2.4.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b1c663ddc641f4192e90511bec61a09bc231e3bbdb996cdc6edbcaa0e528d685", size = 6546954, upload-time = "2026-05-15T20:23:26.099Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e9/ed667cb12c11ca0adde431f685d3a5dd78e6f78b27228c581c8415198e9e/numpy-2.4.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93793222b524f692f12b2f8752ce8b1d9d9125b2bfd5dbf0fb69c92c5e1ce86c", size = 15669430, upload-time = "2026-05-15T20:23:28.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/e5/679f6ffeb01294b0008e5ada4a113cb47617bc0e1819a529fd7973c6d7f4/numpy-2.4.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1616bde34b2bcba2fa9bde06217ce00da4f3d1bdfb264d54525a99e8fe170d83", size = 16633390, upload-time = "2026-05-15T20:23:31.622Z" }, - { url = "https://files.pythonhosted.org/packages/36/46/42bfffc9a780ec902ccd7470d3219192ee82b7b442710307dd85b4d121b0/numpy-2.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09d7d97da1c2c62f4818b3e150a57572ff8dcf1cf5ac501aac832ffd4ebd9566", size = 17020709, upload-time = "2026-05-15T20:23:34.08Z" }, - { url = "https://files.pythonhosted.org/packages/44/00/3e840bfee0cc6cec22209f2c97057f26eeb30de031e4933b4dfc0395416c/numpy-2.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d68d0b355ab2e39fe0de59001d7151dfdbbb880ef67baeed806661e03df5097", size = 18357818, upload-time = "2026-05-15T20:23:36.965Z" }, - { url = "https://files.pythonhosted.org/packages/72/cb/3447b400b9da84134575486f0f656541559b00d4b262477bce9b678bbca8/numpy-2.4.5-cp313-cp313-win32.whl", hash = "sha256:fe28b64777ddfa0eca9b5f51474034ebe3dcb8324f48f27b28f479085673ae33", size = 5961114, upload-time = "2026-05-15T20:23:39.586Z" }, - { url = "https://files.pythonhosted.org/packages/28/f9/a90d2220ffcdc0798f5d55bb5d5463cd6254ec9ef43f384dae80217d7a2f/numpy-2.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:fb4a6c9c537d6ccec9cc4aeae4261bd3cc79b070c67ddc0646f5b1c07fddde42", size = 12318553, upload-time = "2026-05-15T20:23:41.436Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c9/96f531fb3234545315152d34efdf3de7daee81254448447eb619e8d16967/numpy-2.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:6d7df2da2e7ea0624a43aa368104b3a3ce14aae98ad4bb2c9a93fecef76f1c97", size = 10222200, upload-time = "2026-05-15T20:23:43.681Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f4/a291caab5a3c520babf93ff77c54fd5fdb1ebbc3296cee2eb2146ce773b1/numpy-2.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:2a235607a18df941760a695927051af4b1cd5d3ee85840d0e2af816785771feb", size = 14821438, upload-time = "2026-05-15T20:23:45.911Z" }, - { url = "https://files.pythonhosted.org/packages/85/26/13dbb1159b864370568e7309063fd72667984df89db74e9caeb175d067c7/numpy-2.4.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:58dcf64969d870f36bc7fbd557d2617e997db7dc06261b6e3327148ea460d0a4", size = 5326663, upload-time = "2026-05-15T20:23:48.18Z" }, - { url = "https://files.pythonhosted.org/packages/7c/99/d233408072a0e019e2288e27edd23f7d572ccd4a73d1539baa3270ede85d/numpy-2.4.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:235f54b0156274d8fa3155db3ed6d2f401c7e8f3367c90db0a12f02a58fde6ed", size = 6646874, upload-time = "2026-05-15T20:23:49.856Z" }, - { url = "https://files.pythonhosted.org/packages/c5/00/eeb6f193dfe767725e952e0464f3e51f44145c5dd261cd7389aa36ac0713/numpy-2.4.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3b5bb65437a3555c648e706475db01c645559ca80dc8b03e4f202ea757e0d6", size = 15728147, upload-time = "2026-05-15T20:23:51.655Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c9/b8ed039f1fde1b13a8807c893e7e2f9432a379f4d6401edecf0028da5b2c/numpy-2.4.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f09a7e5f017d7098c66522097c96257411c9620c0926212200d66bc8cee3976", size = 16681770, upload-time = "2026-05-15T20:23:53.933Z" }, - { url = "https://files.pythonhosted.org/packages/11/5b/0198ef6cb7016eca6d895d392106012138127fab23f46637e76d5e25c9f5/numpy-2.4.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:993a88d8fdd8554466a8765cd8bacd97ba56b70ca6b0a04bcdca77f5afed4222", size = 17086218, upload-time = "2026-05-15T20:23:56.646Z" }, - { url = "https://files.pythonhosted.org/packages/f0/fe/8821f3cfc660ae84c92ee158505941874b62c56a42e035a41425228cd8cf/numpy-2.4.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:84f58bed609b5669f5ad3d597901a4f1f86ee5b3c3708aaa55f05b4fe6e0f656", size = 18403542, upload-time = "2026-05-15T20:23:59.173Z" }, - { url = "https://files.pythonhosted.org/packages/0e/00/e64ecaf498865e7b091f57658b2c522503e5d1b70e43b807f5f8247e1d88/numpy-2.4.5-cp313-cp313t-win32.whl", hash = "sha256:7200c58f3f933ca61e66346667dcc8510bb111995e9ce15398a731e6a4afa4bb", size = 6084903, upload-time = "2026-05-15T20:24:01.506Z" }, - { url = "https://files.pythonhosted.org/packages/20/c0/354997dedaf74e8311c2cf9a6027b476fd8d424cb92189cc0ae2b25f501c/numpy-2.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c26c71080d35db5002102f5d9ff614d45de02aa1f7802943e691e063e5ee93bc", size = 12458420, upload-time = "2026-05-15T20:24:03.735Z" }, - { url = "https://files.pythonhosted.org/packages/66/dc/917ee5ea4a31ca1a6e4c9a85386477efa318dcc60db257c5ef4adda096c1/numpy-2.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:2caa576d1707b275cba1aeb60a5c50daa6fa2a3f28ecb08123bc05fd439005db", size = 10291826, upload-time = "2026-05-15T20:24:06.535Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c1/3be0bf102fc17cff5bd142e3be0bfffabec6fa46da0a462396c76b0765d0/numpy-2.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:889ca2c072315de638a5194a772aa1fa2df92bdd6175f6a222d4784040424b61", size = 16683455, upload-time = "2026-05-15T20:24:08.988Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3e/0742d724901fa36bc54b338c6e62e463a7601180da896aa44978f0adf004/numpy-2.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89e89304fb1f8c3f0ecfa4a7d48f311dd79771336a940e920159d643d1307e77", size = 14704577, upload-time = "2026-05-15T20:24:11.542Z" }, - { url = "https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:144fcc5a3a17679b2b82543b4a2d8dd29937230a7af13232b5f753872feb6361", size = 5209756, upload-time = "2026-05-15T20:24:14.091Z" }, - { url = "https://files.pythonhosted.org/packages/52/c0/23fb1bc506f774e03db66219a2830e720f4d3dbcaaddf855a7ff7bb6d96f/numpy-2.4.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:398bb16772b265b9fa5c07b07072646ea97137c10ffb62a9a087b277fc825c29", size = 6543937, upload-time = "2026-05-15T20:24:16.223Z" }, - { url = "https://files.pythonhosted.org/packages/9f/49/db4662c26e68520afcc84d672a6f9f5294063dee0e57a46d61afdaa7f9ed/numpy-2.4.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb352e7b8876da1249e72254736d6c58c505fa4e58a3d7e30efca241ca9ca9ce", size = 15685292, upload-time = "2026-05-15T20:24:17.978Z" }, - { url = "https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7341b08ff8124d7353939778e2707b8732d03c78c1c30e0815aba2dacbe1245a", size = 16638528, upload-time = "2026-05-15T20:24:20.478Z" }, - { url = "https://files.pythonhosted.org/packages/56/81/364388600932618fe735d97fdd2437cb8dd87a23377ac11d8b9d5db098b7/numpy-2.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:deb01226f012539f3945261ffe1c10aec081a0fa0a5c925419933c70f3ae2d23", size = 17036709, upload-time = "2026-05-15T20:24:22.949Z" }, - { url = "https://files.pythonhosted.org/packages/32/4a/a1185b18a94a6d9587e54b437e7d0ba36ecf6e614f1bea03f5249912c64e/numpy-2.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d888bdf7335f76878c3c7b264ac1ff089863e211ec81249f9fb5795c2183dc25", size = 18363254, upload-time = "2026-05-15T20:24:25.402Z" }, - { url = "https://files.pythonhosted.org/packages/b9/8e/95c1d2ed15ae97750ede8c8a0ac487c9c01207afff430f47078b1d9d7dc5/numpy-2.4.5-cp314-cp314-win32.whl", hash = "sha256:15f90d1256e9b2320aff24fde44815b787ab6d7c49a1a11bfd8138b321c5f080", size = 6010184, upload-time = "2026-05-15T20:24:27.852Z" }, - { url = "https://files.pythonhosted.org/packages/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4bd2cd4ef9c0afa87de73723c0a33c0edff62143e1432917458e26d3d195d87f", size = 12450344, upload-time = "2026-05-15T20:24:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/3d/64/c0ae481f7c3b2f85869bcd8fc5d30aa7c96b394162eef9c9315957f115c5/numpy-2.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:db304568c650e9d7039744d3575d0d287754debb2057d7c7b8cdfdc2c487a957", size = 10495674, upload-time = "2026-05-15T20:24:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/57/89/c5a4c677acf17aa50ba09a15e61812f90baac42bb6ca38d112e005858351/numpy-2.4.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de2883e0d2c63eae1bab1a84b390dca74aabb3d20ea1f5d58f360853c83abf3", size = 14824078, upload-time = "2026-05-15T20:24:34.669Z" }, - { url = "https://files.pythonhosted.org/packages/e7/52/57e7144284f6b51ba93523e495ff239260b1ecd5257e3700a436332e5688/numpy-2.4.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:06760fe73ae5005008748d182de612c733542af3cde063d532cd2127561b27be", size = 5329246, upload-time = "2026-05-15T20:24:36.957Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b3/09dbce80fd4a7db4318f2fc01eec0ae76f29306442b5a32d4b811d082cdf/numpy-2.4.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:4b51a01745cb04cc19278482207444b4d30728ce91c28d27a3bfae5fc6ff24c7", size = 6649877, upload-time = "2026-05-15T20:24:38.861Z" }, - { url = "https://files.pythonhosted.org/packages/30/c2/dbdb23e82d540b757690ef13f011c386fca6a63848eec6136baf8ce7cbed/numpy-2.4.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a05636d7937d0936f271e5ba957fa8d746b5be3c2025caa1a2508f4fe521d40", size = 15730534, upload-time = "2026-05-15T20:24:41.168Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bd/68f6e9b3c20decf40ac06708a7b506757e3a8588efed32988d1b747316be/numpy-2.4.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b86f56048ed09c3bbe48962a7dff077c2fd3274f8cf981800f3b38eac49cc3", size = 16679741, upload-time = "2026-05-15T20:24:44.874Z" }, - { url = "https://files.pythonhosted.org/packages/39/1d/0fcac0b6b4ea1b50ca8fca05a34bed5c8d56e34c1cb5ffb04cf76109ac3c/numpy-2.4.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:130d58151c4db23e9fa860b84784e219a3aa3e030acc88a493ea37006c4dfd4c", size = 17085598, upload-time = "2026-05-15T20:24:47.603Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e8/a472b2564cf6cc498ad7aa9741d9832648221b8ab8cc0dbef41faa248ede/numpy-2.4.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d475afc8cbe935ff5944f753d863bba774d7f4e1feaaa4102901e3e053ca5963", size = 18403855, upload-time = "2026-05-15T20:24:50.474Z" }, - { url = "https://files.pythonhosted.org/packages/b9/a4/da82196f8cc4bd28ecf17bd57008c84f3d4696caf06753d9bad45e4ad749/numpy-2.4.5-cp314-cp314t-win32.whl", hash = "sha256:27f4a6dc26353a860b348961b9aa9e009835688b435cfa105e873b8dc2c726f5", size = 6156900, upload-time = "2026-05-15T20:24:53.134Z" }, - { url = "https://files.pythonhosted.org/packages/98/31/860959b91a73d9a085006554fa3850da51a7ffab64599bac5097243438ab/numpy-2.4.5-cp314-cp314t-win_amd64.whl", hash = "sha256:76ac6e90f5e226011c88f9b7040a4bcae612518bc7e9adc127e697a13b28ad1a", size = 12638906, upload-time = "2026-05-15T20:24:55.009Z" }, - { url = "https://files.pythonhosted.org/packages/9e/2a/bbd3097913083ad07c0f28fc9629666221fc18923e17ce97ae22a5dccdd6/numpy-2.4.5-cp314-cp314t-win_arm64.whl", hash = "sha256:7c392e2c1bf596701d3c6832be7567eab5d5b0a13865036c33365ee097d37f8b", size = 10565875, upload-time = "2026-05-15T20:24:57.425Z" }, -] - -[[package]] -name = "pillow" -version = "12.2.0" -source = { registry = "https://piwheels.org/simple" } -resolution-markers = [ - "(python_full_version >= '3.14' and platform_machine == 'armv7l') or (python_full_version >= '3.14' and platform_machine == 'armv8l')", - "(python_full_version < '3.14' and platform_machine == 'armv7l') or (python_full_version < '3.14' and platform_machine == 'armv8l')", -] -wheels = [ - { url = "https://piwheels.org/simple/pillow/pillow-12.2.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:d4ff90fbce9831907b35a54405d8b1312c1b83e29f72cc45cf02e60ee55dd48b" }, -] - -[[package]] -name = "pillow" -version = "12.2.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", - "python_full_version < '3.14' and platform_machine != 'armv7l' and platform_machine != 'armv8l'", -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, -] - -[[package]] -name = "pydantic" -version = "2.13.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, -] - -[package.optional-dependencies] -email = [ - { name = "email-validator" }, -] - -[[package]] -name = "pydantic-core" -version = "2.46.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, -] - -[[package]] -name = "pydantic-extra-types" -version = "2.11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/82/c8cd43a6e0719bf5a3b034f6726dd701f75829c08944c83d4b95d02ed0e8/python_multipart-0.0.30.tar.gz", hash = "sha256:0edfe0475c1f46ddd3ff7785a626f6118af32bdcf359bb21260367313bb32118", size = 46316, upload-time = "2026-05-31T19:24:55.198Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/fd/0318007beb234790993d3ec5afd051d1dbceb733e81e3afe2b981ece3f37/python_multipart-0.0.30-py3-none-any.whl", hash = "sha256:830964def8c90607ac5daa00514e3987815865713ade8d20febc9177ac0c3c5b", size = 29730, upload-time = "2026-05-31T19:24:53.814Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - -[[package]] -name = "rich-toolkit" -version = "0.20.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/49/d7a4fd4f39c195b73f78694af3e812943a4181a8d48a11035425d0f6d71f/rich_toolkit-0.20.0.tar.gz", hash = "sha256:bb05382554d4f46865dfca2fccccf30768ef37e0347207d00f034d9b36b25021", size = 203144, upload-time = "2026-06-02T21:11:38.48Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/b5/6b6efd9e305653fae68ed0b712bc659cd3c5541ec54416e6bb14af52acca/rich_toolkit-0.20.0-py3-none-any.whl", hash = "sha256:906e5b8741fafc46159c5f719fd30fd3c9dd8f2c31b8161dc8c612f98b8da01a", size = 35379, upload-time = "2026-06-02T21:11:37.564Z" }, -] - -[[package]] -name = "rignore" -version = "0.7.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, - { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, - { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, - { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, - { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, - { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, - { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, - { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, - { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, - { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, - { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, - { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, - { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, - { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, - { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, - { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, - { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, - { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, - { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, - { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, - { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, - { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, - { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, - { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, - { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, - { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, - { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, - { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, -] - -[[package]] -name = "ruff" -version = "0.15.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, - { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, - { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, - { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, - { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, - { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, - { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, - { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, -] - -[[package]] -name = "sentry-sdk" -version = "2.61.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, -] - -[[package]] -name = "starlette" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/44/ec35f1b6e83094b997da438a02c8c9b0ade2b1e84cfc48bd4656780760a6/starlette-1.2.1.tar.gz", hash = "sha256:9b9b5ebb992e67d6093741e63c2f59e4f6fff986f81163c087867bd7b924b3f6", size = 2701854, upload-time = "2026-05-31T01:07:51.847Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" }, -] - -[[package]] -name = "typer" -version = "0.26.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "rich" }, - { name = "shellingham" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/8dc5733b8939e7f1a71173091a6e27a1658345edbff548a0bf3f5bb26173/typer-0.26.6.tar.gz", hash = "sha256:cdbc160fe7e795b835fb6016419494a521a67bfb86b9476a1ccd0e7727d3ae5b", size = 201595, upload-time = "2026-06-02T13:47:50.536Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/c7/519138260db5e2fe03a509bf9e8ef6af9a514d3565c8fa74fc4fededbae1/typer-0.26.6-py3-none-any.whl", hash = "sha256:49f96d9ee5730cef607bbe155042f40b41fa4c0d0dec04990d580837493805be", size = 122464, upload-time = "2026-06-02T13:47:51.768Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.48.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, - { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, - { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, - { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, - { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, - { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, - { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, - { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, - { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, - { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, - { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, - { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, -] - -[[package]] -name = "watchfiles" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, - { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, - { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, - { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, - { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, - { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, - { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, - { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, - { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, - { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, - { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, - { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, - { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, - { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, - { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, - { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, - { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, - { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, - { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, - { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, - { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, - { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, - { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, - { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, - { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, - { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, - { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, - { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, - { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, - { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, -] - -[[package]] -name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, -] diff --git a/apps/halidoscope/frontend/eslint.config.mjs b/apps/halidoscope/eslint.config.mjs similarity index 100% rename from apps/halidoscope/frontend/eslint.config.mjs rename to apps/halidoscope/eslint.config.mjs diff --git a/apps/halidoscope/frontend/README.md b/apps/halidoscope/frontend/README.md deleted file mode 100644 index 3a142a74fdf3..000000000000 --- a/apps/halidoscope/frontend/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Tauri + React + Typescript - -This template should help get you started developing with Tauri, React and -Typescript in Vite. - -## Recommended IDE Setup - -- [VS Code](https://code.visualstudio.com/) + - [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) - \+ - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/apps/halidoscope/frontend/index.html b/apps/halidoscope/index.html similarity index 100% rename from apps/halidoscope/frontend/index.html rename to apps/halidoscope/index.html diff --git a/apps/halidoscope/frontend/package.json b/apps/halidoscope/package.json similarity index 100% rename from apps/halidoscope/frontend/package.json rename to apps/halidoscope/package.json diff --git a/apps/halidoscope/frontend/pnpm-lock.yaml b/apps/halidoscope/pnpm-lock.yaml similarity index 100% rename from apps/halidoscope/frontend/pnpm-lock.yaml rename to apps/halidoscope/pnpm-lock.yaml diff --git a/apps/halidoscope/frontend/pnpm-workspace.yaml b/apps/halidoscope/pnpm-workspace.yaml similarity index 100% rename from apps/halidoscope/frontend/pnpm-workspace.yaml rename to apps/halidoscope/pnpm-workspace.yaml diff --git a/apps/halidoscope/frontend/src-tauri/.gitignore b/apps/halidoscope/src-tauri/.gitignore similarity index 100% rename from apps/halidoscope/frontend/src-tauri/.gitignore rename to apps/halidoscope/src-tauri/.gitignore diff --git a/apps/halidoscope/frontend/src-tauri/Cargo.lock b/apps/halidoscope/src-tauri/Cargo.lock similarity index 99% rename from apps/halidoscope/frontend/src-tauri/Cargo.lock rename to apps/halidoscope/src-tauri/Cargo.lock index 0c5baafe2628..7d78f17b5c77 100644 --- a/apps/halidoscope/frontend/src-tauri/Cargo.lock +++ b/apps/halidoscope/src-tauri/Cargo.lock @@ -1114,19 +1114,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "frontend" -version = "0.1.0" -dependencies = [ - "colorous", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-cli", - "tauri-plugin-opener", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -1504,6 +1491,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "halidoscope" +version = "0.1.0" +dependencies = [ + "colorous", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-cli", + "tauri-plugin-opener", +] + [[package]] name = "hashbrown" version = "0.12.3" diff --git a/apps/halidoscope/frontend/src-tauri/Cargo.toml b/apps/halidoscope/src-tauri/Cargo.toml similarity index 85% rename from apps/halidoscope/frontend/src-tauri/Cargo.toml rename to apps/halidoscope/src-tauri/Cargo.toml index 9322723ac0d0..d436f7a2af86 100644 --- a/apps/halidoscope/frontend/src-tauri/Cargo.toml +++ b/apps/halidoscope/src-tauri/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "frontend" +name = "halidoscope" version = "0.1.0" -description = "A Tauri App" -authors = ["you"] +description = "An interactive visualizer for Halide traces." +authors = ["Parker Ziegler "] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,7 +11,7 @@ edition = "2021" # The `_lib` suffix may seem redundant but it is necessary # to make the lib name unique and wouldn't conflict with the bin name. # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 -name = "frontend_lib" +name = "halidoscope_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] @@ -30,7 +30,7 @@ tauri-plugin-cli = "2.0.0" # `tauri dev` builds with the dev profile, whose default opt-level = 0 leaves the # trace parser (a tight binary loop over millions of packets) badly unoptimized # — multi-minute loads on large traces. Optimize codegen while keeping fast -# incremental builds and debuggability. This is the single biggest startup win. +# incremental builds and debuggability. [profile.dev] opt-level = 3 diff --git a/apps/halidoscope/frontend/src-tauri/build.rs b/apps/halidoscope/src-tauri/build.rs similarity index 100% rename from apps/halidoscope/frontend/src-tauri/build.rs rename to apps/halidoscope/src-tauri/build.rs diff --git a/apps/halidoscope/frontend/src-tauri/capabilities/default.json b/apps/halidoscope/src-tauri/capabilities/default.json similarity index 100% rename from apps/halidoscope/frontend/src-tauri/capabilities/default.json rename to apps/halidoscope/src-tauri/capabilities/default.json diff --git a/apps/halidoscope/frontend/src-tauri/icons/128x128.png b/apps/halidoscope/src-tauri/icons/128x128.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/128x128.png rename to apps/halidoscope/src-tauri/icons/128x128.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/128x128@2x.png b/apps/halidoscope/src-tauri/icons/128x128@2x.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/128x128@2x.png rename to apps/halidoscope/src-tauri/icons/128x128@2x.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/32x32.png b/apps/halidoscope/src-tauri/icons/32x32.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/32x32.png rename to apps/halidoscope/src-tauri/icons/32x32.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square107x107Logo.png b/apps/halidoscope/src-tauri/icons/Square107x107Logo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/Square107x107Logo.png rename to apps/halidoscope/src-tauri/icons/Square107x107Logo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square142x142Logo.png b/apps/halidoscope/src-tauri/icons/Square142x142Logo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/Square142x142Logo.png rename to apps/halidoscope/src-tauri/icons/Square142x142Logo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square150x150Logo.png b/apps/halidoscope/src-tauri/icons/Square150x150Logo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/Square150x150Logo.png rename to apps/halidoscope/src-tauri/icons/Square150x150Logo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square284x284Logo.png b/apps/halidoscope/src-tauri/icons/Square284x284Logo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/Square284x284Logo.png rename to apps/halidoscope/src-tauri/icons/Square284x284Logo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square30x30Logo.png b/apps/halidoscope/src-tauri/icons/Square30x30Logo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/Square30x30Logo.png rename to apps/halidoscope/src-tauri/icons/Square30x30Logo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square310x310Logo.png b/apps/halidoscope/src-tauri/icons/Square310x310Logo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/Square310x310Logo.png rename to apps/halidoscope/src-tauri/icons/Square310x310Logo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square44x44Logo.png b/apps/halidoscope/src-tauri/icons/Square44x44Logo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/Square44x44Logo.png rename to apps/halidoscope/src-tauri/icons/Square44x44Logo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square71x71Logo.png b/apps/halidoscope/src-tauri/icons/Square71x71Logo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/Square71x71Logo.png rename to apps/halidoscope/src-tauri/icons/Square71x71Logo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/Square89x89Logo.png b/apps/halidoscope/src-tauri/icons/Square89x89Logo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/Square89x89Logo.png rename to apps/halidoscope/src-tauri/icons/Square89x89Logo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/StoreLogo.png b/apps/halidoscope/src-tauri/icons/StoreLogo.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/StoreLogo.png rename to apps/halidoscope/src-tauri/icons/StoreLogo.png diff --git a/apps/halidoscope/frontend/src-tauri/icons/icon.icns b/apps/halidoscope/src-tauri/icons/icon.icns similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/icon.icns rename to apps/halidoscope/src-tauri/icons/icon.icns diff --git a/apps/halidoscope/frontend/src-tauri/icons/icon.ico b/apps/halidoscope/src-tauri/icons/icon.ico similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/icon.ico rename to apps/halidoscope/src-tauri/icons/icon.ico diff --git a/apps/halidoscope/frontend/src-tauri/icons/icon.png b/apps/halidoscope/src-tauri/icons/icon.png similarity index 100% rename from apps/halidoscope/frontend/src-tauri/icons/icon.png rename to apps/halidoscope/src-tauri/icons/icon.png diff --git a/apps/halidoscope/frontend/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs similarity index 100% rename from apps/halidoscope/frontend/src-tauri/src/commands.rs rename to apps/halidoscope/src-tauri/src/commands.rs diff --git a/apps/halidoscope/frontend/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs similarity index 100% rename from apps/halidoscope/frontend/src-tauri/src/lib.rs rename to apps/halidoscope/src-tauri/src/lib.rs diff --git a/apps/halidoscope/frontend/src-tauri/src/main.rs b/apps/halidoscope/src-tauri/src/main.rs similarity index 85% rename from apps/halidoscope/frontend/src-tauri/src/main.rs rename to apps/halidoscope/src-tauri/src/main.rs index e26959fff12b..a9b01cbb75ec 100644 --- a/apps/halidoscope/frontend/src-tauri/src/main.rs +++ b/apps/halidoscope/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - frontend_lib::run() + halidoscope_lib::run() } diff --git a/apps/halidoscope/frontend/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs similarity index 100% rename from apps/halidoscope/frontend/src-tauri/src/render.rs rename to apps/halidoscope/src-tauri/src/render.rs diff --git a/apps/halidoscope/frontend/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs similarity index 100% rename from apps/halidoscope/frontend/src-tauri/src/trace.rs rename to apps/halidoscope/src-tauri/src/trace.rs diff --git a/apps/halidoscope/frontend/src-tauri/tauri.conf.json b/apps/halidoscope/src-tauri/tauri.conf.json similarity index 100% rename from apps/halidoscope/frontend/src-tauri/tauri.conf.json rename to apps/halidoscope/src-tauri/tauri.conf.json diff --git a/apps/halidoscope/frontend/src/App.css b/apps/halidoscope/src/App.css similarity index 100% rename from apps/halidoscope/frontend/src/App.css rename to apps/halidoscope/src/App.css diff --git a/apps/halidoscope/frontend/src/App.tsx b/apps/halidoscope/src/App.tsx similarity index 100% rename from apps/halidoscope/frontend/src/App.tsx rename to apps/halidoscope/src/App.tsx diff --git a/apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx b/apps/halidoscope/src/components/controls/ControlPanel.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/controls/ControlPanel.tsx rename to apps/halidoscope/src/components/controls/ControlPanel.tsx diff --git a/apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx b/apps/halidoscope/src/components/controls/ControlTabs.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/controls/ControlTabs.tsx rename to apps/halidoscope/src/components/controls/ControlTabs.tsx diff --git a/apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx b/apps/halidoscope/src/components/controls/funcs/FuncsPanel.tsx similarity index 99% rename from apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx rename to apps/halidoscope/src/components/controls/funcs/FuncsPanel.tsx index 48ea3345bbfb..c49c74ed3dea 100644 --- a/apps/halidoscope/frontend/src/components/controls/funcs/FuncsPanel.tsx +++ b/apps/halidoscope/src/components/controls/funcs/FuncsPanel.tsx @@ -16,7 +16,7 @@ function FuncsPanel({ funcs }: FuncsPanelProps) { type="single" collapsible className="flex w-full flex-col px-3 py-2 text-xs" - value={func ?? undefined} + value={func} onValueChange={(value) => setFunc(value)} > {Object.values(funcs).map((func) => ( diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/GraphDisplay.tsx b/apps/halidoscope/src/components/controls/visualizations/GraphDisplay.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/controls/visualizations/GraphDisplay.tsx rename to apps/halidoscope/src/components/controls/visualizations/GraphDisplay.tsx diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx b/apps/halidoscope/src/components/controls/visualizations/Histogram.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/controls/visualizations/Histogram.tsx rename to apps/halidoscope/src/components/controls/visualizations/Histogram.tsx diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/HistogramSelect.tsx b/apps/halidoscope/src/components/controls/visualizations/HistogramSelect.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/controls/visualizations/HistogramSelect.tsx rename to apps/halidoscope/src/components/controls/visualizations/HistogramSelect.tsx diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/PlaybackRate.tsx b/apps/halidoscope/src/components/controls/visualizations/PlaybackRate.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/controls/visualizations/PlaybackRate.tsx rename to apps/halidoscope/src/components/controls/visualizations/PlaybackRate.tsx diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx b/apps/halidoscope/src/components/controls/visualizations/VisualizationsPanel.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsPanel.tsx rename to apps/halidoscope/src/components/controls/visualizations/VisualizationsPanel.tsx diff --git a/apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx b/apps/halidoscope/src/components/controls/visualizations/VisualizationsSelect.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/controls/visualizations/VisualizationsSelect.tsx rename to apps/halidoscope/src/components/controls/visualizations/VisualizationsSelect.tsx diff --git a/apps/halidoscope/frontend/src/components/shared/Canvas.tsx b/apps/halidoscope/src/components/shared/Canvas.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/shared/Canvas.tsx rename to apps/halidoscope/src/components/shared/Canvas.tsx diff --git a/apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx b/apps/halidoscope/src/components/shared/HandleCircle.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/shared/HandleCircle.tsx rename to apps/halidoscope/src/components/shared/HandleCircle.tsx diff --git a/apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx b/apps/halidoscope/src/components/views/tracer/FuncCanvas.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/views/tracer/FuncCanvas.tsx rename to apps/halidoscope/src/components/views/tracer/FuncCanvas.tsx diff --git a/apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx b/apps/halidoscope/src/components/views/tracer/Tracer.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/views/tracer/Tracer.tsx rename to apps/halidoscope/src/components/views/tracer/Tracer.tsx diff --git a/apps/halidoscope/frontend/src/components/views/tracer/TracerTimeline.tsx b/apps/halidoscope/src/components/views/tracer/TracerTimeline.tsx similarity index 100% rename from apps/halidoscope/frontend/src/components/views/tracer/TracerTimeline.tsx rename to apps/halidoscope/src/components/views/tracer/TracerTimeline.tsx diff --git a/apps/halidoscope/frontend/src/hooks/trace.ts b/apps/halidoscope/src/hooks/trace.ts similarity index 100% rename from apps/halidoscope/frontend/src/hooks/trace.ts rename to apps/halidoscope/src/hooks/trace.ts diff --git a/apps/halidoscope/frontend/src/main.tsx b/apps/halidoscope/src/main.tsx similarity index 100% rename from apps/halidoscope/frontend/src/main.tsx rename to apps/halidoscope/src/main.tsx diff --git a/apps/halidoscope/frontend/src/state/func.ts b/apps/halidoscope/src/state/func.ts similarity index 100% rename from apps/halidoscope/frontend/src/state/func.ts rename to apps/halidoscope/src/state/func.ts diff --git a/apps/halidoscope/frontend/src/state/graph.ts b/apps/halidoscope/src/state/graph.ts similarity index 100% rename from apps/halidoscope/frontend/src/state/graph.ts rename to apps/halidoscope/src/state/graph.ts diff --git a/apps/halidoscope/frontend/src/state/histogram.ts b/apps/halidoscope/src/state/histogram.ts similarity index 100% rename from apps/halidoscope/frontend/src/state/histogram.ts rename to apps/halidoscope/src/state/histogram.ts diff --git a/apps/halidoscope/frontend/src/state/packet.ts b/apps/halidoscope/src/state/packet.ts similarity index 100% rename from apps/halidoscope/frontend/src/state/packet.ts rename to apps/halidoscope/src/state/packet.ts diff --git a/apps/halidoscope/frontend/src/state/playback.ts b/apps/halidoscope/src/state/playback.ts similarity index 100% rename from apps/halidoscope/frontend/src/state/playback.ts rename to apps/halidoscope/src/state/playback.ts diff --git a/apps/halidoscope/frontend/src/state/visualization.ts b/apps/halidoscope/src/state/visualization.ts similarity index 100% rename from apps/halidoscope/frontend/src/state/visualization.ts rename to apps/halidoscope/src/state/visualization.ts diff --git a/apps/halidoscope/frontend/src/types/index.ts b/apps/halidoscope/src/types/index.ts similarity index 100% rename from apps/halidoscope/frontend/src/types/index.ts rename to apps/halidoscope/src/types/index.ts diff --git a/apps/halidoscope/frontend/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts similarity index 100% rename from apps/halidoscope/frontend/src/utils/api.ts rename to apps/halidoscope/src/utils/api.ts diff --git a/apps/halidoscope/frontend/src/utils/constants.ts b/apps/halidoscope/src/utils/constants.ts similarity index 100% rename from apps/halidoscope/frontend/src/utils/constants.ts rename to apps/halidoscope/src/utils/constants.ts diff --git a/apps/halidoscope/frontend/src/utils/graph.ts b/apps/halidoscope/src/utils/graph.ts similarity index 100% rename from apps/halidoscope/frontend/src/utils/graph.ts rename to apps/halidoscope/src/utils/graph.ts diff --git a/apps/halidoscope/frontend/src/vite-env.d.ts b/apps/halidoscope/src/vite-env.d.ts similarity index 100% rename from apps/halidoscope/frontend/src/vite-env.d.ts rename to apps/halidoscope/src/vite-env.d.ts diff --git a/apps/halidoscope/frontend/tsconfig.json b/apps/halidoscope/tsconfig.json similarity index 100% rename from apps/halidoscope/frontend/tsconfig.json rename to apps/halidoscope/tsconfig.json diff --git a/apps/halidoscope/frontend/tsconfig.node.json b/apps/halidoscope/tsconfig.node.json similarity index 100% rename from apps/halidoscope/frontend/tsconfig.node.json rename to apps/halidoscope/tsconfig.node.json diff --git a/apps/halidoscope/frontend/vite.config.ts b/apps/halidoscope/vite.config.ts similarity index 100% rename from apps/halidoscope/frontend/vite.config.ts rename to apps/halidoscope/vite.config.ts From 5002b3d3d9c5182845262558d53446893079ab59 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 25 Jun 2026 15:02:21 -0700 Subject: [PATCH 14/67] Support produce-consume liveness highlighting and refactor backend with mode-specific renderers. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/package.json | 4 +- apps/halidoscope/src-tauri/src/commands.rs | 255 +++++---- apps/halidoscope/src-tauri/src/lib.rs | 8 +- apps/halidoscope/src-tauri/src/render.rs | 486 +++++++++++------- apps/halidoscope/src-tauri/src/trace.rs | 66 ++- apps/halidoscope/src/App.css | 3 +- .../src/components/controls/ControlPanel.tsx | 39 -- .../components/controls/ControlSection.tsx | 22 + .../src/components/controls/ControlTabs.tsx | 12 +- .../controls/{funcs => }/FuncsPanel.tsx | 0 ...ationsPanel.tsx => VisualizationPanel.tsx} | 81 ++- .../GraphDisplay.tsx | 0 .../Histogram.tsx | 1 + .../HistogramSelect.tsx | 4 +- .../controls/liveness/LivenessControls.tsx | 61 +++ .../PlaybackRate.tsx | 0 .../RenderMode.tsx} | 27 +- .../src/components/shared/Canvas.tsx | 53 +- .../components/views/tracer/FuncCanvas.tsx | 92 ++-- apps/halidoscope/src/state/liveness.ts | 5 + apps/halidoscope/src/state/render.ts | 13 + apps/halidoscope/src/state/visualization.ts | 10 - apps/halidoscope/src/types/index.ts | 13 +- apps/halidoscope/src/utils/api.ts | 69 +-- apps/halidoscope/src/utils/liveness.ts | 20 + 25 files changed, 844 insertions(+), 500 deletions(-) delete mode 100644 apps/halidoscope/src/components/controls/ControlPanel.tsx create mode 100644 apps/halidoscope/src/components/controls/ControlSection.tsx rename apps/halidoscope/src/components/controls/{funcs => }/FuncsPanel.tsx (100%) rename apps/halidoscope/src/components/controls/{visualizations/VisualizationsPanel.tsx => VisualizationPanel.tsx} (63%) rename apps/halidoscope/src/components/controls/{visualizations => graph}/GraphDisplay.tsx (100%) rename apps/halidoscope/src/components/controls/{visualizations => histogram}/Histogram.tsx (99%) rename apps/halidoscope/src/components/controls/{visualizations => histogram}/HistogramSelect.tsx (98%) create mode 100644 apps/halidoscope/src/components/controls/liveness/LivenessControls.tsx rename apps/halidoscope/src/components/controls/{visualizations => playback}/PlaybackRate.tsx (100%) rename apps/halidoscope/src/components/controls/{visualizations/VisualizationsSelect.tsx => render/RenderMode.tsx} (67%) create mode 100644 apps/halidoscope/src/state/liveness.ts create mode 100644 apps/halidoscope/src/state/render.ts delete mode 100644 apps/halidoscope/src/state/visualization.ts create mode 100644 apps/halidoscope/src/utils/liveness.ts diff --git a/apps/halidoscope/package.json b/apps/halidoscope/package.json index 41e655fc34e6..29185679ec51 100644 --- a/apps/halidoscope/package.json +++ b/apps/halidoscope/package.json @@ -8,7 +8,9 @@ "build": "tsc && vite build", "preview": "vite preview", "tauri": "tauri", - "format": "prettier --write ." + "format": "prettier --write .", + "check:types": "tsc --noEmit", + "lint": "eslint src/**/*.{ts,tsx}" }, "dependencies": { "@dagrejs/dagre": "^3.0.0", diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index bc726caf67f6..7e2c694a189e 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -5,39 +5,26 @@ use std::collections::{BTreeMap, HashMap}; use std::sync::Mutex; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use tauri::ipc::Response; use tauri::State; -use crate::render::{HeatmapState, RedundantState, RenderState, ReuseDistanceState}; +use crate::render::{ + GrayscaleState, LoadFrequencyState, RedundantState, ReuseDistanceState, RgbState, + StoreFrequencyState, +}; use crate::trace::Trace; -/// How a Func's values are mapped to pixels. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum RenderMode { - Grayscale, - Rgb, +/// A half-open packet-index interval `[start, end]` used for liveness and produce/consume ranges. +#[derive(Debug, Clone, Copy, Serialize)] +pub struct IndexRange { + pub start: u32, + pub end: u32, } -/// Which access type to visualize in a heatmap render. Variant names match the frontend's -/// `VisualizationMode` strings so they can be passed through without conversion. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum HeatmapMode { - #[serde(rename = "Store Frequency")] - Stores, - #[serde(rename = "Load Frequency")] - Loads, -} - -/// The default render mode inferred from a Func's channel count: 3 or 4 channels are treated as -/// color, everything else as grayscale. Shared by the metadata derivation and `render_at` so the -/// inferred default never diverges. -pub fn default_mode(channels: u32) -> RenderMode { - if channels == 3 || channels == 4 { - RenderMode::Rgb - } else { - RenderMode::Grayscale +impl IndexRange { + fn from_tuple((start, end): (u32, u32)) -> Self { + Self { start, end } } } @@ -48,7 +35,6 @@ pub struct FuncMeta { pub width: u32, pub height: u32, pub channels: u32, - pub default_mode: RenderMode, pub num_stores: u32, pub min_coords: Vec, pub max_coords: Vec, @@ -62,8 +48,9 @@ pub struct FuncMeta { pub load_count_histogram: Vec, pub redundant_count_histogram: Vec, pub reuse_distance_histogram: Vec, - pub liveness_start: u32, - pub liveness_end: u32, + pub buffer_liveness: IndexRange, + pub produce_ranges: Vec, + pub consume_ranges: Vec, } /// Top-level payload returned by `open_trace`. @@ -97,31 +84,20 @@ impl TraceMeta { Some(g) => (g.width as u32, g.height as u32, g.channels as u32), None => (0, 0, 1), }; - let default_mode = default_mode(channels); let stores = trace.func_store_indices(name); let num_stores = stores.map(<[usize]>::len).unwrap_or(0) as u32; - if stats.max_store_count > global_max_store_count { - global_max_store_count = stats.max_store_count; - } - if stats.max_load_count > global_max_load_count { - global_max_load_count = stats.max_load_count; - } - if stats.max_redundant_count > global_max_redundant_count { - global_max_redundant_count = stats.max_redundant_count; - } - if stats.max_reuse_distance > global_max_reuse_distance { - global_max_reuse_distance = stats.max_reuse_distance; - } - - let liveness_range = trace.func_liveness_range(name).unwrap_or(&(0, 0)); + global_max_store_count = stats.max_store_count.max(global_max_store_count); + global_max_load_count = stats.max_load_count.max(global_max_load_count); + global_max_redundant_count = + stats.max_redundant_count.max(global_max_redundant_count); + global_max_reuse_distance = stats.max_reuse_distance.max(global_max_reuse_distance); FuncMeta { name: name.clone(), width, height, channels, - default_mode, num_stores, min_coords: stats.min_coords.clone(), max_coords: stats.max_coords.clone(), @@ -135,8 +111,26 @@ impl TraceMeta { load_count_histogram: stats.load_count_histogram.clone(), redundant_count_histogram: stats.redundant_count_histogram.clone(), reuse_distance_histogram: stats.reuse_distance_histogram.clone(), - liveness_start: liveness_range.0, - liveness_end: liveness_range.1, + buffer_liveness: IndexRange::from_tuple( + trace + .func_buffer_liveness_range(name) + .unwrap_or(&(0, 0)) + .clone(), + ), + produce_ranges: trace + .func_produce_ranges(name) + .unwrap_or(&[]) + .iter() + .copied() + .map(IndexRange::from_tuple) + .collect(), + consume_ranges: trace + .func_consume_ranges(name) + .unwrap_or(&[]) + .iter() + .copied() + .map(IndexRange::from_tuple) + .collect(), } }) .collect(); @@ -161,18 +155,20 @@ impl TraceMeta { // ── Tauri-managed state ─────────────────────────────────────────────────────── -/// The currently loaded trace plus a per-Func render cache. The cache keeps each Func's -/// framebuffer warm across requests so forward scrubbing only applies the delta of new stores. +/// The currently loaded trace plus per-Func render caches, one map per rendering pathway. Each +/// cache keeps its Func's state warm across requests so forward scrubbing only applies the delta. struct Loaded { trace: Trace, - renderers: HashMap, - heatmap_renderers: HashMap, + grayscale_renderers: HashMap, + rgb_renderers: HashMap, + store_frequency_renderers: HashMap, + load_frequency_renderers: HashMap, redundant_renderers: HashMap, reuse_distance_renderers: HashMap, } /// App-wide state managed by Tauri. A single trace is loaded at a time; opening a new one replaces -/// it (and drops the stale render cache). +/// it (and drops all stale render caches). #[derive(Default)] pub struct AppState { inner: Mutex>, @@ -190,92 +186,148 @@ pub fn open_trace(path: String, state: State) -> Result, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; let loaded = guard.as_mut().ok_or("no trace loaded")?; - // Split the borrow so the trace can be read while a renderer is mutated. let Loaded { - trace, renderers, .. + trace, + grayscale_renderers, + .. } = loaded; - // Get or lazily build this Func's render state. - if !renderers.contains_key(&func) { - let rs = RenderState::new(trace, &func) + if !grayscale_renderers.contains_key(&func) { + let rs = GrayscaleState::new(trace, &func) .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; - renderers.insert(func.clone(), rs); + grayscale_renderers.insert(func.clone(), rs); } - let renderer = renderers.get_mut(&func).expect("just inserted"); + let renderer = grayscale_renderers.get_mut(&func).expect("just inserted"); - // Resolve the global timeline index into a store count: how many of this Func's stores have - // occurred by `global_index` (inclusive). let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); let k = store_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, store_indices, k); - let mode = mode.unwrap_or_else(|| default_mode(renderer.channels() as u32)); - Ok(Response::new(renderer.to_rgba(mode))) + Ok(Response::new(renderer.to_rgba())) } -/// Renders a heatmap of store or load counts for `func` up to `global_index` and returns raw RGBA8 -/// bytes. Mirrors `render_at` — forward seeks apply only the new events; backward seeks clear and -/// replay. Counts are normalized against the per-Func full-trace maximum so the color scale is stable. +/// Renders `func` as an RGB image at `global_index` and returns raw RGBA8 bytes. Planes 0/1/2 +/// map to R/G/B; missing planes default to 0. #[tauri::command] -pub fn render_heatmap( +pub fn render_rgb( func: String, global_index: u32, - mode: HeatmapMode, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; let loaded = guard.as_mut().ok_or("no trace loaded")?; let Loaded { trace, - heatmap_renderers, + rgb_renderers, .. } = loaded; - if !heatmap_renderers.contains_key(&func) { - let hs = HeatmapState::new(trace, &func, mode) + if !rgb_renderers.contains_key(&func) { + let rs = RgbState::new(trace, &func) .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; - heatmap_renderers.insert(func.clone(), hs); + rgb_renderers.insert(func.clone(), rs); } - let hs = heatmap_renderers.get_mut(&func).expect("just inserted"); + let renderer = rgb_renderers.get_mut(&func).expect("just inserted"); - let event_indices = match mode { - HeatmapMode::Stores => trace.func_store_indices(&func).unwrap_or(&[]), - HeatmapMode::Loads => trace.func_load_indices(&func).unwrap_or(&[]), - }; - let k = event_indices.partition_point(|&p| p <= global_index as usize); - hs.seek(trace, event_indices, k, mode); + let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); + let k = store_indices.partition_point(|&p| p <= global_index as usize); + renderer.seek(trace, store_indices, k); - Ok(Response::new(hs.to_rgba())) + Ok(Response::new(renderer.to_rgba())) +} + +/// Renders a heatmap of store counts for `func` up to `global_index` and returns raw RGBA8 bytes. +/// Counts are normalized against the global full-trace maximum so the color scale is stable while +/// scrubbing. +#[tauri::command] +pub fn render_store_frequency( + func: String, + global_index: u32, + state: State, +) -> Result { + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + let loaded = guard.as_mut().ok_or("no trace loaded")?; + let Loaded { + trace, + store_frequency_renderers, + .. + } = loaded; + + if !store_frequency_renderers.contains_key(&func) { + let hs = StoreFrequencyState::new(trace, &func) + .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; + store_frequency_renderers.insert(func.clone(), hs); + } + let renderer = store_frequency_renderers + .get_mut(&func) + .expect("just inserted"); + + let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); + let k = store_indices.partition_point(|&p| p <= global_index as usize); + renderer.seek(trace, store_indices, k); + + Ok(Response::new(renderer.to_rgba())) +} + +/// Renders a heatmap of load counts for `func` up to `global_index` and returns raw RGBA8 bytes. +/// Counts are normalized against the global full-trace maximum so the color scale is stable while +/// scrubbing. +#[tauri::command] +pub fn render_load_frequency( + func: String, + global_index: u32, + state: State, +) -> Result { + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + let loaded = guard.as_mut().ok_or("no trace loaded")?; + let Loaded { + trace, + load_frequency_renderers, + .. + } = loaded; + + if !load_frequency_renderers.contains_key(&func) { + let hs = LoadFrequencyState::new(trace, &func) + .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; + load_frequency_renderers.insert(func.clone(), hs); + } + let renderer = load_frequency_renderers + .get_mut(&func) + .expect("just inserted"); + + let load_indices = trace.func_load_indices(&func).unwrap_or(&[]); + let k = load_indices.partition_point(|&p| p <= global_index as usize); + renderer.seek(trace, load_indices, k); + + Ok(Response::new(renderer.to_rgba())) } /// Renders a heatmap of redundant store counts for `func` up to `global_index` and returns raw -/// RGBA8 bytes. A store is redundant when it writes the same value to a location that already holds -/// that value. Counts are normalized against the per-Func full-trace maximum so the scale is stable -/// while scrubbing. Pixels with zero redundant stores are black; positive counts map through the -/// Reds colormap. +/// RGBA8 bytes. A store is redundant when it writes the same value to a location that already +/// holds that value. Pixels with zero redundant stores are black; positive counts map through the +/// Inferno colormap normalized against the global full-trace maximum. #[tauri::command] -pub fn render_redundant( +pub fn render_redundant_stores( func: String, global_index: u32, state: State, @@ -293,20 +345,19 @@ pub fn render_redundant( .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; redundant_renderers.insert(func.clone(), rs); } - let rs = redundant_renderers.get_mut(&func).expect("just inserted"); + let renderer = redundant_renderers.get_mut(&func).expect("just inserted"); let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); let k = store_indices.partition_point(|&p| p <= global_index as usize); - rs.seek(trace, store_indices, k); + renderer.seek(trace, store_indices, k); - Ok(Response::new(rs.to_rgba())) + Ok(Response::new(renderer.to_rgba())) } /// Renders a heatmap of maximum store-to-load reuse distances for `func` up to `global_index` /// and returns raw RGBA8 bytes. Reuse distance is the number of packets elapsed between a store -/// and the next load from the same (x, y, channel). Both event streams are merged in global order -/// so the two-pointer seek reflects the correct pairing. Pixels with no observed store→load pair -/// are black; positive distances map through the Inferno colormap normalized against the per-Func +/// and the next load from the same (x, y, channel). Pixels with no observed store→load pair are +/// black; positive distances map through the Inferno colormap normalized against the global /// full-trace maximum. #[tauri::command] pub fn render_reuse_distance( @@ -327,7 +378,7 @@ pub fn render_reuse_distance( .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; reuse_distance_renderers.insert(func.clone(), rs); } - let rs = reuse_distance_renderers + let renderer = reuse_distance_renderers .get_mut(&func) .expect("just inserted"); @@ -335,7 +386,7 @@ pub fn render_reuse_distance( let load_indices = trace.func_load_indices(&func).unwrap_or(&[]); let store_k = store_indices.partition_point(|&p| p <= global_index as usize); let load_k = load_indices.partition_point(|&p| p <= global_index as usize); - rs.seek(trace, store_indices, load_indices, store_k, load_k); + renderer.seek(trace, store_indices, load_indices, store_k, load_k); - Ok(Response::new(rs.to_rgba())) + Ok(Response::new(renderer.to_rgba())) } diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index b246862855bb..d4a21d6ed9a4 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -27,9 +27,11 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ get_cwd, commands::open_trace, - commands::render_at, - commands::render_heatmap, - commands::render_redundant, + commands::render_grayscale, + commands::render_rgb, + commands::render_store_frequency, + commands::render_load_frequency, + commands::render_redundant_stores, commands::render_reuse_distance ]) .run(tauri::generate_context!()) diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index c6b57c0ec2cb..f7f3a370bb11 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -1,37 +1,34 @@ //! Framebuffer rendering for Halidoscope. //! -//! A `RenderState` holds the accumulated pixel state for a single Func after applying the first -//! `applied_k` of its store events, plus everything needed to normalize and emit RGBA. It is -//! deliberately decoupled from `Trace`: the packets and the Func's store-index list are passed -//! into `seek`, so the state can live in Tauri-managed state alongside the (separately owned) -//! parsed trace without a self-referential borrow. +//! Each rendering pathway has its own state type that accumulates pixel data from trace events +//! and emits RGBA8 for `putImageData`. States are decoupled from `Trace`: event index slices are +//! passed into `seek` so states can live in Tauri-managed storage alongside the parsed trace. + use ::colorous; -use crate::commands::{HeatmapMode, RenderMode}; use crate::trace::{pixel_xy, FuncGeometry, Trace, TracePacket}; -pub struct RenderState { +// ── Grayscale rendering ─────────────────────────────────────────────────────── + +/// Accumulated pixel state for a single Func. Channel 0 is normalized to [0, 255] and replicated +/// across R/G/B in `to_rgba`. Forward seeks apply only the delta; backward seeks clear and replay. +pub struct GrayscaleState { geom: FuncGeometry, min_v: f64, max_v: f64, - /// Latest normalized intensity per (pixel, channel), row-major with the channel as the minor - /// axis: `framebuffer[(y * width + x) * channels + c]`. Length is `width * height * channels`. - /// Unwritten cells stay 0 (black). + /// Latest normalized intensity per (pixel, channel), row-major with channel as the minor axis. + /// Length is `width * height * channels`. Unwritten cells stay 0. framebuffer: Vec, - /// Number of this Func's store events currently reflected in `framebuffer`. applied_k: usize, } -impl RenderState { - /// Builds an empty render state for `func`, or `None` if the Func has no usable geometry (no - /// coordinate extent / zero area). +impl GrayscaleState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let stats = trace.funcs.get(func)?; let min_v = stats.min_value.unwrap_or(0.0); let max_v = stats.max_value.unwrap_or(255.0); let framebuffer = vec![0u8; geom.width * geom.height * geom.channels]; - Some(Self { geom, min_v, @@ -41,25 +38,18 @@ impl RenderState { }) } - /// Brings the framebuffer to the state after the first `target_k` stores. Forward seeks apply - /// only the delta (`applied_k..target_k`); backward seeks clear and replay from zero. - /// `store_indices` is the Func's global packet-index list (from `Trace::func_store_indices`). pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { let target_k = target_k.min(store_indices.len()); - if target_k < self.applied_k { - // Rewind: no per-pixel history is kept, so reset and replay forward. self.framebuffer.iter_mut().for_each(|b| *b = 0); self.applied_k = 0; } - for &global_idx in &store_indices[self.applied_k..target_k] { self.apply_store(&trace.packets[global_idx]); } self.applied_k = target_k; } - /// Writes one store packet's lanes into the framebuffer (last write wins). fn apply_store(&mut self, pkt: &TracePacket) { let n_lanes = pkt.type_.lanes.max(1) as usize; let dims_per_lane = pkt.coordinates.len() / n_lanes; @@ -72,7 +62,6 @@ impl RenderState { min_c, .. } = self.geom; - for lane in 0..n_lanes { let Some(v) = pkt.decoded_value(lane) else { continue; @@ -81,7 +70,6 @@ impl RenderState { if x < 0 || y < 0 || x as usize >= width || y as usize >= height { continue; } - // Channel is logical dim 2 when present; otherwise the single plane 0. let c = if dims_per_lane >= 3 { pkt.coordinates[2 * n_lanes + lane] - min_c } else { @@ -95,7 +83,6 @@ impl RenderState { } } - /// Maps a decoded value to a [0, 255] intensity. #[inline] fn normalize(&self, v: f64) -> u8 { if self.max_v > self.min_v { @@ -105,116 +92,77 @@ impl RenderState { } } - /// Produces a `width * height * 4` RGBA8 buffer ready for `putImageData`. `Rgb` maps planes - /// 0/1/2 to R/G/B (missing planes are 0); `Grayscale` replicates plane 0 across R/G/B. - /// Alpha is always opaque. - pub fn to_rgba(&self, mode: RenderMode) -> Vec { + /// Produces a `width * height * 4` RGBA8 buffer. Channel 0 is replicated across R/G/B. + pub fn to_rgba(&self) -> Vec { let FuncGeometry { width, height, channels, .. } = self.geom; - let pixels = width * height; - let mut out = vec![0u8; pixels * 4]; - - // Hoist the mode branch and the channel-count checks outside the pixel loop so each - // inner loop is branch-free and LLVM can auto-vectorize it. + let mut out = vec![0u8; width * height * 4]; let fb = &self.framebuffer; - match mode { - RenderMode::Grayscale => { - for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { - let v = src[0]; - chunk[0] = v; - chunk[1] = v; - chunk[2] = v; - chunk[3] = 255; - } + if channels >= 3 { + for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { + // Use grayscale weights from scikit-image: + // https://scikit-image.org/docs/stable/auto_examples/color_exposure/plot_rgb_to_gray.html + let gray = src[0] as f64 * 0.2125 + src[1] as f64 * 0.7154 + src[2] as f64 * 0.0721; + chunk[0] = gray as u8; + chunk[1] = gray as u8; + chunk[2] = gray as u8; + chunk[3] = 255; } - RenderMode::Rgb => { - if channels >= 3 { - for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { - chunk[0] = src[0]; - chunk[1] = src[1]; - chunk[2] = src[2]; - chunk[3] = 255; - } - } else if channels == 2 { - for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { - chunk[0] = src[0]; - chunk[1] = src[1]; - chunk[2] = 0; - chunk[3] = 255; - } - } else { - for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { - chunk[0] = src[0]; - chunk[1] = 0; - chunk[2] = 0; - chunk[3] = 255; - } - } + } else { + for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { + chunk[0] = src[0]; + chunk[1] = src[0]; + chunk[2] = src[0]; + chunk[3] = 255; } } out } - /// Number of channel planes (logical dim 2 extent, or 1). pub fn channels(&self) -> usize { self.geom.channels } } -// ── Redundant computation rendering ────────────────────────────────────────── +// ── RGB rendering ───────────────────────────────────────────────────────────── -/// Accumulated per-pixel redundant-store counts for one Func. A store to pixel (x, y, c) is -/// redundant when it writes the same bit-pattern that was last stored there. The full-trace max -/// redundant count (pre-computed at parse time) is used for normalization so the color scale is -/// stable across the entire scrub range. -pub struct RedundantState { +/// Same accumulation logic as `GrayscaleState`, but `to_rgba` maps planes 0/1/2 directly to +/// R/G/B. Missing planes default to 0. Alpha is always opaque. +pub struct RgbState { geom: FuncGeometry, - /// Last value stored per (pixel × channel), flat row-major: - /// `last_values[(y * width + x) * channels + c]`. - /// `None` = no store has landed here yet. - last_values: Vec>, - /// Redundant-store count per spatial pixel, indexed by `y * width + x`. - redundant_counts: Vec, - global_max_redundant_count: i32, + min_v: f64, + max_v: f64, + /// Latest normalized intensity per (pixel, channel), row-major with channel as the minor axis. + /// Length is `width * height * channels`. Unwritten cells stay 0. + framebuffer: Vec, applied_k: usize, } -impl RedundantState { - /// Builds an empty redundant state for `func`, or `None` if the Func has no usable geometry. +impl RgbState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; - let n_pixels = geom.width * geom.height; - let global_max_redundant_count = trace - .funcs - .values() - .map(|s| s.max_redundant_count) - .max() - .unwrap_or(0); + let stats = trace.funcs.get(func)?; + let min_v = stats.min_value.unwrap_or(0.0); + let max_v = stats.max_value.unwrap_or(255.0); + let framebuffer = vec![0u8; geom.width * geom.height * geom.channels]; Some(Self { geom, - last_values: vec![None; n_pixels * geom.channels], - redundant_counts: vec![0i32; n_pixels], - global_max_redundant_count, + min_v, + max_v, + framebuffer, applied_k: 0, }) } - fn reset(&mut self) { - self.last_values.iter_mut().for_each(|v| *v = None); - self.redundant_counts.iter_mut().for_each(|c| *c = 0); - self.applied_k = 0; - } - - /// Seeks to the state after the first `target_k` store events. Forward seeks apply only the - /// delta; backward seeks reset and replay from zero. pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { let target_k = target_k.min(store_indices.len()); if target_k < self.applied_k { - self.reset(); + self.framebuffer.iter_mut().for_each(|b| *b = 0); + self.applied_k = 0; } for &global_idx in &store_indices[self.applied_k..target_k] { self.apply_store(&trace.packets[global_idx]); @@ -234,7 +182,6 @@ impl RedundantState { min_c, .. } = self.geom; - for lane in 0..n_lanes { let Some(v) = pkt.decoded_value(lane) else { continue; @@ -251,77 +198,153 @@ impl RedundantState { if c < 0 || c as usize >= channels { continue; } - let val_idx = (y as usize * width + x as usize) * channels + c as usize; - let pixel_idx = y as usize * width + x as usize; - let v_bits = v.to_bits(); - if let Some(prev_bits) = self.last_values[val_idx] { - if prev_bits == v_bits { - self.redundant_counts[pixel_idx] += 1; - } + let idx = (y as usize * width + x as usize) * channels + c as usize; + self.framebuffer[idx] = self.normalize(v); + } + } + + #[inline] + fn normalize(&self, v: f64) -> u8 { + (255.0 * (v - self.min_v) / (self.max_v - self.min_v)).clamp(0.0, 255.0) as u8 + } + + /// Produces a `width * height * 4` RGBA8 buffer. Planes 0/1/2 map to R/G/B; + /// missing planes are 0. Alpha is always opaque. + pub fn to_rgba(&self) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + let mut out = vec![0u8; width * height * 4]; + let fb = &self.framebuffer; + if channels >= 3 { + for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { + chunk[0] = src[0]; + chunk[1] = src[1]; + chunk[2] = src[2]; + chunk[3] = 255; + } + } else { + for (chunk, src) in out.chunks_exact_mut(4).zip(fb.chunks_exact(channels)) { + chunk[0] = src[0]; + chunk[1] = src[0]; + chunk[2] = src[0]; + chunk[3] = 255; } - self.last_values[val_idx] = Some(v_bits); } + out } - /// Produces a `width × height × 4` RGBA8 buffer. Pixels with zero redundant stores are black; - /// pixels with one or more are mapped through the Reds colormap, normalized against the - /// global full-trace maximum so intensities are comparable across all Funcs. + pub fn channels(&self) -> usize { + self.geom.channels + } +} + +// ── Store frequency rendering ───────────────────────────────────────────────── + +/// Accumulated per-pixel store counts for one Func, seekable along the global timeline. Forward +/// seeks apply only the new events; backward seeks clear and replay. The global max store count +/// is used for normalization so the color scale is stable across the entire scrub range. +pub struct StoreFrequencyState { + geom: FuncGeometry, + counts: Vec, + global_max_store_count: i32, + applied_k: usize, +} + +impl StoreFrequencyState { + pub fn new(trace: &Trace, func: &str) -> Option { + let geom = trace.func_geometry(func)?; + let counts = vec![0i32; geom.width * geom.height]; + let global_max_store_count = trace + .funcs + .values() + .map(|s| s.max_store_count) + .max() + .unwrap_or(0); + Some(Self { + geom, + counts, + global_max_store_count, + applied_k: 0, + }) + } + + pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + if target_k < self.applied_k { + self.counts.iter_mut().for_each(|c| *c = 0); + self.applied_k = 0; + } + for &idx in &store_indices[self.applied_k..target_k] { + self.increment_pixel(&trace.packets[idx]); + } + self.applied_k = target_k; + } + + fn increment_pixel(&mut self, pkt: &TracePacket) { + let FuncGeometry { + width, + height, + min_x, + min_y, + .. + } = self.geom; + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for l in 0..n_lanes { + let (x, y) = pixel_xy(pkt, l, n_lanes, dims_per_lane, min_x, min_y); + if x >= 0 && y >= 0 && (x as usize) < width && (y as usize) < height { + self.counts[y as usize * width + x as usize] += 1; + } + } + } + + /// Produces a `width × height × 4` RGBA8 buffer with the Inferno colormap applied. Counts + /// are normalized against the global full-trace maximum so intensities are comparable across + /// all Funcs. pub fn to_rgba(&self) -> Vec { let FuncGeometry { width, height, .. } = self.geom; - let gradient = colorous::INFERNO; let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { let c = gradient.eval_continuous(i as f64 / 255.0); [c.r, c.g, c.b] }); - let scale = if self.global_max_redundant_count > 0 { - 255.0 / self.global_max_redundant_count as f64 + let scale = if self.global_max_store_count > 0 { + 255.0 / self.global_max_store_count as f64 } else { 0.0 }; - let mut out = vec![0u8; width * height * 4]; - for (chunk, &count) in out.chunks_exact_mut(4).zip(self.redundant_counts.iter()) { - if count > 0 { - let ti = (count as f64 * scale) as usize; - let [r, g, b] = lut[ti.min(255)]; - chunk[0] = r; - chunk[1] = g; - chunk[2] = b; - } + for (chunk, &count) in out.chunks_exact_mut(4).zip(self.counts.iter()) { + let ti = (count as f64 * scale) as usize; + let [r, g, b] = lut[ti.min(255)]; + chunk[0] = r; + chunk[1] = g; + chunk[2] = b; chunk[3] = 255; } out } } -// ── Heatmap rendering ───────────────────────────────────────────────────────── +// ── Load frequency rendering ────────────────────────────────────────────────── -/// Accumulated per-pixel event counts for one Func, seekable along the global timeline. Mirrors -/// `RenderState` but tracks a count per pixel instead of the latest normalized value. Forward -/// seeks apply only the new events; backward seeks clear and replay. The full-trace max count -/// (pre-computed at parse time and stored in `FuncGeometry`) is used for normalization so the -/// color scale is stable across the entire scrub range. -pub struct HeatmapState { +/// Mirrors `StoreFrequencyState` but tracks load events instead of store events. The global max +/// load count is used for normalization so the color scale is stable across the entire scrub range. +pub struct LoadFrequencyState { geom: FuncGeometry, - mode: HeatmapMode, counts: Vec, - global_max_store_count: i32, global_max_load_count: i32, applied_k: usize, } -impl HeatmapState { - /// Builds an empty heatmap state for `func`, or `None` if the Func has no usable geometry. - pub fn new(trace: &Trace, func: &str, mode: HeatmapMode) -> Option { +impl LoadFrequencyState { + pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let counts = vec![0i32; geom.width * geom.height]; - let global_max_store_count = trace - .funcs - .values() - .map(|s| s.max_store_count) - .max() - .unwrap_or(0); let global_max_load_count = trace .funcs .values() @@ -330,37 +353,19 @@ impl HeatmapState { .unwrap_or(0); Some(Self { geom, - mode, counts, - global_max_store_count, global_max_load_count, applied_k: 0, }) } - /// Seeks to the state after the first `target_k` events of `new_mode`. If `new_mode` differs - /// from the cached mode the counts are cleared first. `event_indices` must be the Func's - /// global packet-index list for that mode. - pub fn seek( - &mut self, - trace: &Trace, - event_indices: &[usize], - target_k: usize, - new_mode: HeatmapMode, - ) { - if new_mode != self.mode { - self.counts.iter_mut().for_each(|c| *c = 0); - self.applied_k = 0; - self.mode = new_mode; - } - - let target_k = target_k.min(event_indices.len()); + pub fn seek(&mut self, trace: &Trace, load_indices: &[usize], target_k: usize) { + let target_k = target_k.min(load_indices.len()); if target_k < self.applied_k { self.counts.iter_mut().for_each(|c| *c = 0); self.applied_k = 0; } - - for &idx in &event_indices[self.applied_k..target_k] { + for &idx in &load_indices[self.applied_k..target_k] { self.increment_pixel(&trace.packets[idx]); } self.applied_k = target_k; @@ -384,30 +389,21 @@ impl HeatmapState { } } - /// Produces a `width × height × 4` RGBA8 buffer with the inferno colormap applied. Counts are - /// normalized against the global full-trace maximum so intensities are comparable across all - /// Funcs. + /// Produces a `width × height × 4` RGBA8 buffer with the Inferno colormap applied. Counts + /// are normalized against the global full-trace maximum so intensities are comparable across + /// all Funcs. pub fn to_rgba(&self) -> Vec { let FuncGeometry { width, height, .. } = self.geom; - let max_count = match self.mode { - HeatmapMode::Stores => self.global_max_store_count, - HeatmapMode::Loads => self.global_max_load_count, - }; - - // Build a 256-entry LUT once before the pixel loop. Calling eval_continuous 256 times is - // negligible; doing it once per pixel at 750K+ pixels is the choppy inner loop culprit. - // 256 × 3 bytes = 768 bytes, fits entirely in L1 cache. let gradient = colorous::INFERNO; let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { let c = gradient.eval_continuous(i as f64 / 255.0); [c.r, c.g, c.b] }); - let scale = if max_count > 0 { - 255.0 / max_count as f64 + let scale = if self.global_max_load_count > 0 { + 255.0 / self.global_max_load_count as f64 } else { 0.0 }; - let mut out = vec![0u8; width * height * 4]; for (chunk, &count) in out.chunks_exact_mut(4).zip(self.counts.iter()) { let ti = (count as f64 * scale) as usize; @@ -421,6 +417,136 @@ impl HeatmapState { } } +// ── Redundant computation rendering ────────────────────────────────────────── + +/// Accumulated per-pixel redundant-store counts for one Func. A store to pixel (x, y, c) is +/// redundant when it writes the same bit-pattern that was last stored there. The full-trace max +/// redundant count (pre-computed at parse time) is used for normalization so the color scale is +/// stable across the entire scrub range. +pub struct RedundantState { + geom: FuncGeometry, + /// Last value stored per (pixel × channel), flat row-major: + /// `last_values[(y * width + x) * channels + c]`. + /// `None` = no store has landed here yet. + last_values: Vec>, + /// Redundant-store count per spatial pixel, indexed by `y * width + x`. + redundant_counts: Vec, + global_max_redundant_count: i32, + applied_k: usize, +} + +impl RedundantState { + /// Builds an empty redundant state for `func`, or `None` if the Func has no usable geometry. + pub fn new(trace: &Trace, func: &str) -> Option { + let geom = trace.func_geometry(func)?; + let n_pixels = geom.width * geom.height; + let global_max_redundant_count = trace + .funcs + .values() + .map(|s| s.max_redundant_count) + .max() + .unwrap_or(0); + Some(Self { + geom, + last_values: vec![None; n_pixels * geom.channels], + redundant_counts: vec![0i32; n_pixels], + global_max_redundant_count, + applied_k: 0, + }) + } + + fn reset(&mut self) { + self.last_values.iter_mut().for_each(|v| *v = None); + self.redundant_counts.iter_mut().for_each(|c| *c = 0); + self.applied_k = 0; + } + + /// Seeks to the state after the first `target_k` store events. Forward seeks apply only the + /// delta; backward seeks reset and replay from zero. + pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + if target_k < self.applied_k { + self.reset(); + } + for &global_idx in &store_indices[self.applied_k..target_k] { + self.apply_store(&trace.packets[global_idx]); + } + self.applied_k = target_k; + } + + fn apply_store(&mut self, pkt: &TracePacket) { + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + let FuncGeometry { + width, + height, + channels, + min_x, + min_y, + min_c, + .. + } = self.geom; + + for lane in 0..n_lanes { + let Some(v) = pkt.decoded_value(lane) else { + continue; + }; + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= width || y as usize >= height { + continue; + } + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + if c < 0 || c as usize >= channels { + continue; + } + let val_idx = (y as usize * width + x as usize) * channels + c as usize; + let pixel_idx = y as usize * width + x as usize; + let v_bits = v.to_bits(); + if let Some(prev_bits) = self.last_values[val_idx] { + if prev_bits == v_bits { + self.redundant_counts[pixel_idx] += 1; + } + } + self.last_values[val_idx] = Some(v_bits); + } + } + + /// Produces a `width × height × 4` RGBA8 buffer. Pixels with zero redundant stores are black; + /// pixels with one or more are mapped through the Inferno colormap, normalized against the + /// global full-trace maximum so intensities are comparable across all Funcs. + pub fn to_rgba(&self) -> Vec { + let FuncGeometry { width, height, .. } = self.geom; + + let gradient = colorous::INFERNO; + let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { + let c = gradient.eval_continuous(i as f64 / 255.0); + [c.r, c.g, c.b] + }); + let scale = if self.global_max_redundant_count > 0 { + 255.0 / self.global_max_redundant_count as f64 + } else { + 0.0 + }; + + let mut out = vec![0u8; width * height * 4]; + for (chunk, &count) in out.chunks_exact_mut(4).zip(self.redundant_counts.iter()) { + if count > 0 { + let ti = (count as f64 * scale) as usize; + let [r, g, b] = lut[ti.min(255)]; + chunk[0] = r; + chunk[1] = g; + chunk[2] = b; + } + chunk[3] = 255; + } + out + } +} + // ── Reuse distance rendering ────────────────────────────────────────────────── /// Per-pixel maximum reuse distance for one Func, seekable along the global timeline. @@ -459,7 +585,9 @@ impl ReuseDistanceState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let n_cells = geom.width * geom.height * geom.channels; - let is_input = trace.func_store_indices(func).map_or(true, |s| s.is_empty()); + let is_input = trace + .func_store_indices(func) + .map_or(true, |s| s.is_empty()); let global_max_reuse_distance = trace .funcs .values() diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index b27d1d369e9c..fb5588eb53ad 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -234,7 +234,9 @@ pub struct Trace { pub dag_edges: BTreeMap>, pub store_indices_by_func: BTreeMap>, pub load_indices_by_func: BTreeMap>, - pub liveness_range_by_func: BTreeMap, + pub buffer_liveness_range_by_func: BTreeMap, + pub produce_ranges_by_func: BTreeMap>, + pub consume_ranges_by_func: BTreeMap>, } // ── Binary parsing helpers ──────────────────────────────────────────────────── @@ -429,7 +431,9 @@ impl Trace { let mut dag_edges: BTreeMap> = BTreeMap::new(); let mut store_indices_by_func: BTreeMap> = BTreeMap::new(); let mut load_indices_by_func: BTreeMap> = BTreeMap::new(); - let mut liveness_range_by_func: BTreeMap = BTreeMap::new(); + let mut buffer_liveness_range_by_func: BTreeMap = BTreeMap::new(); + let mut produce_ranges_by_func: BTreeMap> = BTreeMap::new(); + let mut consume_ranges_by_func: BTreeMap> = BTreeMap::new(); // id -> pipeline name: propagated down the parent chain so every event in a pipeline can // compute its qualified name. @@ -549,7 +553,7 @@ impl Trace { // Start the liveness range for this Func at the current packet index. let idx = packets.len() as u32; - liveness_range_by_func + buffer_liveness_range_by_func .entry(qualified.clone()) .and_modify(|range| range.0 = range.0.min(idx)) .or_insert((idx, idx)); @@ -558,7 +562,7 @@ impl Trace { // End the liveness range for this Func at the current packet index. let idx = packets.len() as u32; - liveness_range_by_func + buffer_liveness_range_by_func .entry(qualified.clone()) .and_modify(|range| range.1 = range.1.max(idx)) .or_insert((idx, idx)); @@ -595,6 +599,40 @@ impl Trace { update_coord_range(&pkt, stats); update_value_range(&pkt, stats); } + EventCode::Produce => { + let idx = packets.len() as u32; + + produce_ranges_by_func + .entry(qualified.clone()) + .or_default() + .push((idx, idx)); + } + EventCode::EndProduce => { + let idx = packets.len() as u32; + + if let Some(ranges) = produce_ranges_by_func.get_mut(qualified.as_str()) { + if let Some(last) = ranges.last_mut() { + last.1 = idx; + } + } + } + EventCode::Consume => { + let idx = packets.len() as u32; + + consume_ranges_by_func + .entry(qualified.clone()) + .or_default() + .push((idx, idx)); + } + EventCode::EndConsume => { + let idx = packets.len() as u32; + + if let Some(ranges) = consume_ranges_by_func.get_mut(qualified.as_str()) { + if let Some(last) = ranges.last_mut() { + last.1 = idx; + } + } + } _ => {} } @@ -927,7 +965,9 @@ impl Trace { dag_edges, store_indices_by_func, load_indices_by_func, - liveness_range_by_func, + buffer_liveness_range_by_func, + produce_ranges_by_func, + consume_ranges_by_func, }) } @@ -944,8 +984,20 @@ impl Trace { self.load_indices_by_func.get(qualified).map(Vec::as_slice) } - pub fn func_liveness_range(&self, qualified: &str) -> Option<&(u32, u32)> { - self.liveness_range_by_func.get(qualified) + pub fn func_buffer_liveness_range(&self, qualified: &str) -> Option<&(u32, u32)> { + self.buffer_liveness_range_by_func.get(qualified) + } + + pub fn func_produce_ranges(&self, qualified: &str) -> Option<&[(u32, u32)]> { + self.produce_ranges_by_func + .get(qualified) + .map(Vec::as_slice) + } + + pub fn func_consume_ranges(&self, qualified: &str) -> Option<&[(u32, u32)]> { + self.consume_ranges_by_func + .get(qualified) + .map(Vec::as_slice) } /// Spatial layout for `qualified`, or `None` if it has no usable coordinate extent. Reuses diff --git a/apps/halidoscope/src/App.css b/apps/halidoscope/src/App.css index 9faa1b664070..ada85e03319b 100644 --- a/apps/halidoscope/src/App.css +++ b/apps/halidoscope/src/App.css @@ -41,7 +41,8 @@ input[type="number"] { --color-ps-border-secondary: oklch(0.3979 0 0); --color-ps-border-tertiary: oklch(0.4997 0 0); --color-highlight: oklch(0.77 0.1919 163.7); - + --color-produce: oklch(0.837 0.14 75); + --color-consume: oklch(0.74 0.175 305.4); --text-tiny: 0.625rem; --text-tiny--line-height: 1.5; } diff --git a/apps/halidoscope/src/components/controls/ControlPanel.tsx b/apps/halidoscope/src/components/controls/ControlPanel.tsx deleted file mode 100644 index c1617787b4cd..000000000000 --- a/apps/halidoscope/src/components/controls/ControlPanel.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Checkbox } from "radix-ui"; - -interface ControlPanelProps { - setHidden: React.Dispatch>; -} - -function ControlPanel({ setHidden }: ControlPanelProps) { - return ( -
-
- setHidden(checked === false)} - > - - - - - - - -
-
- ); -} - -export default ControlPanel; diff --git a/apps/halidoscope/src/components/controls/ControlSection.tsx b/apps/halidoscope/src/components/controls/ControlSection.tsx new file mode 100644 index 000000000000..3ca8207efcac --- /dev/null +++ b/apps/halidoscope/src/components/controls/ControlSection.tsx @@ -0,0 +1,22 @@ +import { Label } from "radix-ui"; +import type * as React from "react"; + +interface ControlSectionProps { + title: string; +} + +function ControlSection({ + title, + children, +}: React.PropsWithChildren) { + return ( +
+ + {title} + +
{children}
+
+ ); +} + +export default ControlSection; diff --git a/apps/halidoscope/src/components/controls/ControlTabs.tsx b/apps/halidoscope/src/components/controls/ControlTabs.tsx index 92da02fe5071..0385ecc72123 100644 --- a/apps/halidoscope/src/components/controls/ControlTabs.tsx +++ b/apps/halidoscope/src/components/controls/ControlTabs.tsx @@ -1,7 +1,7 @@ import { Tabs } from "radix-ui"; -import FuncsPanel from "@/components/controls/funcs/FuncsPanel"; -import VisualizationsPanel from "@/components/controls/visualizations/VisualizationsPanel"; +import FuncsPanel from "@/components/controls/FuncsPanel"; +import VisualizationPanel from "@/components/controls/VisualizationPanel"; import { FuncMeta } from "@/types"; function ControlTabs({ funcs }: { funcs: Record }) { @@ -21,17 +21,17 @@ function ControlTabs({ funcs }: { funcs: Record }) { Funcs - Visualizations + Visualization - - + +
diff --git a/apps/halidoscope/src/components/controls/funcs/FuncsPanel.tsx b/apps/halidoscope/src/components/controls/FuncsPanel.tsx similarity index 100% rename from apps/halidoscope/src/components/controls/funcs/FuncsPanel.tsx rename to apps/halidoscope/src/components/controls/FuncsPanel.tsx diff --git a/apps/halidoscope/src/components/controls/visualizations/VisualizationsPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx similarity index 63% rename from apps/halidoscope/src/components/controls/visualizations/VisualizationsPanel.tsx rename to apps/halidoscope/src/components/controls/VisualizationPanel.tsx index 7dd3367cc601..33469867c6ee 100644 --- a/apps/halidoscope/src/components/controls/visualizations/VisualizationsPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -1,41 +1,39 @@ import { useAtomValue } from "jotai"; +import { Separator } from "radix-ui"; import * as React from "react"; -import { Label, Separator } from "radix-ui"; -import GraphDisplay from "@/components/controls/visualizations/GraphDisplay"; -import Histogram from "@/components/controls/visualizations/Histogram"; -import HistogramSelect from "@/components/controls/visualizations/HistogramSelect"; -import PlaybackRate from "@/components/controls/visualizations/PlaybackRate"; -import VisualizationSelect from "@/components/controls/visualizations/VisualizationsSelect"; +import ControlSection from "@/components/controls/ControlSection"; +import GraphDisplay from "@/components/controls/graph/GraphDisplay"; +import LivenessControls from "@/components/controls/liveness/LivenessControls"; +import PlaybackRate from "@/components/controls/playback/PlaybackRate"; +import RenderMode from "@/components/controls/render/RenderMode"; +import Histogram from "@/components/controls/histogram/Histogram"; +import HistogramSelect from "@/components/controls/histogram/HistogramSelect"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; import { histogramAtom, type HistogramScale } from "@/state/histogram"; -import { - type VisualizationMode, - visualizationModeAtom, -} from "@/state/visualization"; +import { type RenderMode as RM, renderModeAtom } from "@/state/render"; import { FuncMeta } from "@/types"; -const VISUALIZATION_MODE_TO_HISTOGRAM_DATA_KEY: Record< - VisualizationMode, - keyof FuncMeta | "" -> = { - "True Values": "", +const RENDER_MODE_TO_HISTOGRAM_DATA_KEY: Record = { + Grayscale: "", + RGB: "", "Store Frequency": "store_count_histogram", "Load Frequency": "load_count_histogram", "Redundant Stores": "redundant_count_histogram", "Reuse Distance": "reuse_distance_histogram", }; -const VISUALIZATION_MODE_TO_LABEL: Record = { - "True Values": "", +const RENDER_MODE_TO_LABEL: Record = { + Grayscale: "", + RGB: "", "Store Frequency": "Store Count", "Load Frequency": "Load Count", "Redundant Stores": "Redundant Store Count", "Reuse Distance": "Reuse Distance (Packets)", }; -function VisualizationsPanel() { +function VisualizationPanel() { const { funcs, globalMaxStoreCount, @@ -43,11 +41,11 @@ function VisualizationsPanel() { globalMaxRedundantCount, globalMaxReuseDistance, } = useTraceContext(); - const visualizationMode = useAtomValue(visualizationModeAtom); + const renderMode = useAtomValue(renderModeAtom); const activeFunc = useAtomValue(funcAtom); const histogramScale = useAtomValue(histogramAtom) as HistogramScale; - const dataKey = VISUALIZATION_MODE_TO_HISTOGRAM_DATA_KEY[visualizationMode]; + const dataKey = RENDER_MODE_TO_HISTOGRAM_DATA_KEY[renderMode]; const hasHistogram = dataKey && activeFunc && funcs[activeFunc]; const domainMin = histogramScale === "log" ? 1 : 0; @@ -61,7 +59,7 @@ function VisualizationsPanel() { const data = funcs[activeFunc][dataKey as keyof FuncMeta] as number[]; - switch (visualizationMode) { + switch (renderMode) { case "Store Frequency": return { data: data.map((pixels, i) => ({ x1: i, x2: i + 1, y: pixels })), @@ -96,7 +94,7 @@ function VisualizationsPanel() { activeFunc, funcs, dataKey, - visualizationMode, + renderMode, domainMin, globalMaxStoreCount, globalMaxLoadCount, @@ -106,43 +104,38 @@ function VisualizationsPanel() { return (
-
- - Visualization - - -
+ + + {hasHistogram ? ( <> -
- - Histogram - +
-
+ ) : null} -
- - Parameters - - + + + + + -
+ + + + +
); } -export default VisualizationsPanel; +export default VisualizationPanel; diff --git a/apps/halidoscope/src/components/controls/visualizations/GraphDisplay.tsx b/apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx similarity index 100% rename from apps/halidoscope/src/components/controls/visualizations/GraphDisplay.tsx rename to apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx diff --git a/apps/halidoscope/src/components/controls/visualizations/Histogram.tsx b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx similarity index 99% rename from apps/halidoscope/src/components/controls/visualizations/Histogram.tsx rename to apps/halidoscope/src/components/controls/histogram/Histogram.tsx index dbf01b298f47..8a1b6b6a95bf 100644 --- a/apps/halidoscope/src/components/controls/visualizations/Histogram.tsx +++ b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx @@ -14,6 +14,7 @@ interface HistogramProps { } function Histogram({ data, domain, labels }: HistogramProps) { + console.log("Data: ", data); const ref = React.useRef(null); const histogramScale = useAtomValue(histogramAtom) as HistogramScale; // Build the data for the bottom colorbar. diff --git a/apps/halidoscope/src/components/controls/visualizations/HistogramSelect.tsx b/apps/halidoscope/src/components/controls/histogram/HistogramSelect.tsx similarity index 98% rename from apps/halidoscope/src/components/controls/visualizations/HistogramSelect.tsx rename to apps/halidoscope/src/components/controls/histogram/HistogramSelect.tsx index 27005b5dc5ed..eda07d9ec204 100644 --- a/apps/halidoscope/src/components/controls/visualizations/HistogramSelect.tsx +++ b/apps/halidoscope/src/components/controls/histogram/HistogramSelect.tsx @@ -24,7 +24,9 @@ function HistogramSelect() { id="func-select" className="bg-ps-border-primary text-ps-text-primary border-ps-border-tertiary inline-flex h-8 w-full items-center justify-center rounded border px-2 uppercase focus:outline-none" > - + + + setLivenessMode(value as LivenessMode)} + > +
+ + + + +
+
+ + + + +
+
+ + + + +
+ + ); +} + +export default LivenessControls; diff --git a/apps/halidoscope/src/components/controls/visualizations/PlaybackRate.tsx b/apps/halidoscope/src/components/controls/playback/PlaybackRate.tsx similarity index 100% rename from apps/halidoscope/src/components/controls/visualizations/PlaybackRate.tsx rename to apps/halidoscope/src/components/controls/playback/PlaybackRate.tsx diff --git a/apps/halidoscope/src/components/controls/visualizations/VisualizationsSelect.tsx b/apps/halidoscope/src/components/controls/render/RenderMode.tsx similarity index 67% rename from apps/halidoscope/src/components/controls/visualizations/VisualizationsSelect.tsx rename to apps/halidoscope/src/components/controls/render/RenderMode.tsx index cf4d56ec7e6a..bfc3011bb249 100644 --- a/apps/halidoscope/src/components/controls/visualizations/VisualizationsSelect.tsx +++ b/apps/halidoscope/src/components/controls/render/RenderMode.tsx @@ -1,30 +1,15 @@ import { Select } from "radix-ui"; import { useAtom } from "jotai"; -import { - visualizationModeAtom, - type VisualizationMode, -} from "@/state/visualization"; - -const VISUALIZATION_MODES = [ - { value: "True Values", label: "True Values" }, - { value: "Store Frequency", label: "Store Frequency" }, - { value: "Load Frequency", label: "Load Frequency" }, - { value: "Redundant Stores", label: "Redundant Stores" }, - { value: "Reuse Distance", label: "Reuse Distance" }, -] as const; +import { renderModeAtom, RENDER_MODES, type RenderMode } from "@/state/render"; function VisualizationSelect() { - const [visualizationMode, setVisualizationMode] = useAtom( - visualizationModeAtom, - ); + const [renderMode, setVisualizationMode] = useAtom(renderModeAtom); return ( - setVisualizationMode(value as VisualizationMode) - } + value={renderMode} + onValueChange={(value) => setVisualizationMode(value as RenderMode)} > - {VISUALIZATION_MODES.map(({ value, label }) => ( + {RENDER_MODES.map((value) => ( - {label} + {value} ))} diff --git a/apps/halidoscope/src/components/shared/Canvas.tsx b/apps/halidoscope/src/components/shared/Canvas.tsx index 919725cc3d8c..e02466a15a00 100644 --- a/apps/halidoscope/src/components/shared/Canvas.tsx +++ b/apps/halidoscope/src/components/shared/Canvas.tsx @@ -7,14 +7,17 @@ import { type Edge, type EdgeChange, } from "@xyflow/react"; -import { useAtom, useSetAtom } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import * as React from "react"; import FuncCanvas from "@/components/views/tracer/FuncCanvas"; import { funcAtom } from "@/state/func"; import { edgesAtom } from "@/state/graph"; +import { livenessAtom } from "@/state/liveness"; +import { packetAtom } from "@/state/packet"; import { FuncMeta, NodeTypes } from "@/types"; import { buildEdges, buildNodes, getLayoutedElements } from "@/utils/graph"; +import { isFuncConsuming, isFuncProducing } from "@/utils/liveness"; const NODE_TYPES = { funcCanvas: FuncCanvas, @@ -47,6 +50,52 @@ function Canvas({ funcs, dagEdges, type }: CanvasProps) { [setEdges], ); + const livenessMode = useAtomValue(livenessAtom); + const packetIndex = useAtomValue(packetAtom); + + const consumingFuncs = React.useMemo(() => { + if (livenessMode !== "produce-consume") { + return new Set(); + } + + const fs = new Set(); + for (const [funcName, func] of Object.entries(funcs)) { + if (isFuncConsuming(func, packetIndex)) { + fs.add(funcName); + } + } + + return fs; + }, [livenessMode, funcs, packetIndex]); + + const producingFuncs = React.useMemo(() => { + if (livenessMode !== "produce-consume") { + return new Set(); + } + + const fs = new Set(); + for (const [funcName, func] of Object.entries(funcs)) { + if (isFuncProducing(func, packetIndex)) { + fs.add(funcName); + } + } + + return fs; + }, [livenessMode, funcs, packetIndex]); + + const styledEdges = React.useMemo(() => { + return edges.map((edge) => { + if (consumingFuncs.has(edge.source) && producingFuncs.has(edge.target)) { + return { + ...edge, + style: { stroke: "var(--color-produce)" }, + }; + } + + return edge; + }); + }, [edges, consumingFuncs, producingFuncs]); + const { zoom } = useViewport(); React.useEffect(() => { @@ -57,7 +106,7 @@ function Canvas({ funcs, dagEdges, type }: CanvasProps) {
; -function FuncCanvas({ - data: { name, width, height, liveness_start, liveness_end }, -}: NodeProps) { +function FuncCanvas({ data }: NodeProps) { + const { name, width, height } = data; const canvasRef = React.useRef(null); - const globalIndex = useAtomValue(packetAtom); - const visualizationMode = useAtomValue(visualizationModeAtom); - const isFuncBufferLive = React.useMemo( - () => liveness_start <= globalIndex && globalIndex <= liveness_end, - [liveness_start, liveness_end, globalIndex], + + const livenessMode = useAtomValue(livenessAtom); + const packetIndex = useAtomValue(packetAtom); + const renderMode = useAtomValue(renderModeAtom); + + const bufferLive = React.useMemo( + () => + livenessMode === "realizations" && isFuncBufferLive(data, packetIndex), + [livenessMode, data, packetIndex], + ); + const producing = React.useMemo( + () => + livenessMode === "produce-consume" && isFuncProducing(data, packetIndex), + [livenessMode, data, packetIndex], + ); + const consuming = React.useMemo( + () => + livenessMode === "produce-consume" && isFuncConsuming(data, packetIndex), + [livenessMode, data, packetIndex], ); const nodes = useNodes(); @@ -53,11 +74,11 @@ function FuncCanvas({ // Together these coalesce rapid scrub updates: while a frame is in flight, // newer indices just overwrite `latestIndexRef`, and the loop renders only // the most recent one rather than every intermediate position. - const latestIndexRef = React.useRef(globalIndex); + const latestIndexRef = React.useRef(packetIndex); const renderingRef = React.useRef(false); React.useEffect(() => { - latestIndexRef.current = globalIndex; + latestIndexRef.current = packetIndex; if (renderingRef.current) { return; @@ -70,19 +91,24 @@ function FuncCanvas({ let buffer: ArrayBuffer; - switch (visualizationMode) { - case "True Values": - buffer = await renderAt(name, target); + switch (renderMode) { + case "Grayscale": + buffer = await renderGrayscale(name, target); + break; + case "RGB": + buffer = await renderRgb(name, target); break; case "Store Frequency": + buffer = await renderStoreFrequency(name, target); + break; case "Load Frequency": - buffer = await renderHeatmap(name, target, visualizationMode); + buffer = await renderLoadFrequency(name, target); break; case "Reuse Distance": buffer = await renderReuseDistance(name, target); break; case "Redundant Stores": - buffer = await renderRedundant(name, target); + buffer = await renderRedundantStores(name, target); break; } @@ -105,7 +131,7 @@ function FuncCanvas({ } render(); - }, [globalIndex, name, width, height, visualizationMode]); + }, [packetIndex, name, width, height, renderMode]); return (
@@ -113,24 +139,28 @@ function FuncCanvas({ {name}
= 1, + className={clsx("ring-transparent", { + "ring-highlight/30!": bufferLive, + "ring-produce/30!": producing, + "ring-consume/30!": consuming, + "ring-4": zoom < 1, + "ring-2": zoom >= 1, })} > = 1, + className={clsx("ring-transparent", { + "ring-highlight!": bufferLive, + "ring-produce!": producing, + "ring-consume!": consuming, + "ring-2": zoom < 1, + "ring-1": zoom >= 1, })} />
- {incomingEdgeCount > 0 ? ( + {incomingEdgeCount > 0 && edges.every((edge) => !edge.hidden) ? ( ) : null} - {outgoingEdgeCount > 0 ? ( + {outgoingEdgeCount > 0 && edges.every((edge) => !edge.hidden) ? ( ("none"); diff --git a/apps/halidoscope/src/state/render.ts b/apps/halidoscope/src/state/render.ts new file mode 100644 index 000000000000..9c388c89ac65 --- /dev/null +++ b/apps/halidoscope/src/state/render.ts @@ -0,0 +1,13 @@ +import { atom } from "jotai"; + +export const RENDER_MODES = [ + "Grayscale", + "RGB", + "Store Frequency", + "Load Frequency", + "Redundant Stores", + "Reuse Distance", +] as const; +export type RenderMode = (typeof RENDER_MODES)[number]; + +export const renderModeAtom = atom("Grayscale"); diff --git a/apps/halidoscope/src/state/visualization.ts b/apps/halidoscope/src/state/visualization.ts deleted file mode 100644 index c8ba66847f64..000000000000 --- a/apps/halidoscope/src/state/visualization.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { atom } from "jotai"; - -export type VisualizationMode = - | "True Values" - | "Store Frequency" - | "Load Frequency" - | "Redundant Stores" - | "Reuse Distance"; - -export const visualizationModeAtom = atom("True Values"); diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts index d3452cd04e35..aa5c46754eae 100644 --- a/apps/halidoscope/src/types/index.ts +++ b/apps/halidoscope/src/types/index.ts @@ -1,5 +1,8 @@ -/** How a Func's values are mapped to pixels. Mirrors the Rust `RenderMode`. */ -export type RenderMode = "grayscale" | "rgb"; +/** A packet-index interval `[start, end]`. Mirrors the Rust `IndexRange`. */ +export interface IndexRange { + start: number; + end: number; +} /** * Per-Func metadata returned by the `open_trace` command. Mirrors the Rust @@ -11,7 +14,6 @@ export interface FuncMeta extends Record { width: number; height: number; channels: number; - default_mode: RenderMode; num_stores: number; min_coords: number[]; max_coords: number[]; @@ -25,8 +27,9 @@ export interface FuncMeta extends Record { load_count_histogram: number[]; redundant_count_histogram: number[]; reuse_distance_histogram: number[]; - liveness_start: number; - liveness_end: number; + buffer_liveness: IndexRange; + produce_ranges: IndexRange[]; + consume_ranges: IndexRange[]; } /** Top-level payload returned by `open_trace`. Mirrors the Rust `TraceMeta`. */ diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index bd59c4df8175..0f6721e02cf0 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -1,73 +1,46 @@ import { invoke } from "@tauri-apps/api/core"; -import type { RenderMode, TraceMeta } from "../types"; -import type { VisualizationMode } from "../state/visualization"; +import type { TraceMeta } from "@/types"; -/** - * Parse a Halide trace from disk and return its metadata. - * - * @param path Absolute path to the `.hltrace` file. - * @returns The {@link TraceMeta} describing every Func and the global timeline. - * @throws If the backend fails to read or parse the trace. - */ export async function openTrace(path: string): Promise { return invoke("open_trace", { path }); } -/** - * Render a Func's framebuffer state at a point on the global timeline. - * - * The backend accumulates the Func's stores up to `globalIndex` and returns a - * `width * height * 4` RGBA8 buffer (delivered as an `ArrayBuffer`), ready to - * hand to `putImageData`. - * - * @param func The qualified Func name. - * @param globalIndex Position on the global packet timeline. - * @param mode Optional override of the Func's inferred render mode. - * @returns The raw RGBA8 bytes for the frame. - */ -export async function renderAt( +export async function renderGrayscale( func: string, globalIndex: number, - mode?: RenderMode, ): Promise { - return invoke("render_at", { func, globalIndex, mode }); + return invoke("render_grayscale", { func, globalIndex }); } -/** - * Render a heatmap of store or load counts for `func` up to `globalIndex`. - * The mode must be "Store Frequency" or "Load Frequency" — the string is - * passed directly to the Rust backend. Returns a `width * height * 4` RGBA8 - * buffer with the inferno colormap applied. - */ -export async function renderHeatmap( +export async function renderRgb( func: string, globalIndex: number, - mode: Exclude, ): Promise { - return invoke("render_heatmap", { func, globalIndex, mode }); + return invoke("render_rgb", { func, globalIndex }); } -/** - * Render a heatmap of redundant store counts for `func` up to `globalIndex`. - * A store is redundant when it writes the same value to a location that already - * holds that value. Returns a `width * height * 4` RGBA8 buffer with the Reds - * colormap applied; pixels with zero redundant stores are black. - */ -export async function renderRedundant( +export async function renderStoreFrequency( func: string, globalIndex: number, ): Promise { - return invoke("render_redundant", { func, globalIndex }); + return invoke("render_store_frequency", { func, globalIndex }); +} + +export async function renderLoadFrequency( + func: string, + globalIndex: number, +): Promise { + return invoke("render_load_frequency", { func, globalIndex }); +} + +export async function renderRedundantStores( + func: string, + globalIndex: number, +): Promise { + return invoke("render_redundant_stores", { func, globalIndex }); } -/** - * Render a heatmap of maximum store-to-load reuse distances for `func` up to - * `globalIndex`. Reuse distance is measured in total packets elapsed between a - * store and the next load from the same (x, y, channel). Returns a - * `width * height * 4` RGBA8 buffer; pixels with no store→load pair are black, - * positive distances map through the Inferno colormap. - */ export async function renderReuseDistance( func: string, globalIndex: number, diff --git a/apps/halidoscope/src/utils/liveness.ts b/apps/halidoscope/src/utils/liveness.ts new file mode 100644 index 000000000000..402c10241451 --- /dev/null +++ b/apps/halidoscope/src/utils/liveness.ts @@ -0,0 +1,20 @@ +import type { FuncMeta } from "@/types"; + +export function isFuncBufferLive(func: FuncMeta, globalIndex: number) { + return ( + func.buffer_liveness.start <= globalIndex && + globalIndex <= func.buffer_liveness.end + ); +} + +export function isFuncConsuming(func: FuncMeta, globalIndex: number) { + return func.consume_ranges.some( + (range) => range.start <= globalIndex && globalIndex <= range.end, + ); +} + +export function isFuncProducing(func: FuncMeta, globalIndex: number) { + return func.produce_ranges.some( + (range) => range.start <= globalIndex && globalIndex <= range.end, + ); +} From 5f66e2f804168cef3d9c758094c5a76feb9241d0 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Fri, 26 Jun 2026 15:12:59 -0700 Subject: [PATCH 15/67] Add CLI for snapshotting Func buffers at packet indices. --- apps/halidoscope/.prettierignore | 3 +- apps/halidoscope/src-tauri/Cargo.lock | 658 +++++++++++++++++++++ apps/halidoscope/src-tauri/Cargo.toml | 1 + apps/halidoscope/src-tauri/src/cli.rs | 165 ++++++ apps/halidoscope/src-tauri/src/commands.rs | 2 +- apps/halidoscope/src-tauri/src/lib.rs | 19 +- apps/halidoscope/src-tauri/src/render.rs | 181 +++--- apps/halidoscope/src-tauri/src/trace.rs | 16 +- apps/halidoscope/src-tauri/tauri.conf.json | 53 +- 9 files changed, 1008 insertions(+), 90 deletions(-) create mode 100644 apps/halidoscope/src-tauri/src/cli.rs diff --git a/apps/halidoscope/.prettierignore b/apps/halidoscope/.prettierignore index 0b01a51456dc..42152d9a2864 100644 --- a/apps/halidoscope/.prettierignore +++ b/apps/halidoscope/.prettierignore @@ -1,4 +1,5 @@ node_modules -/src-tauri/ +/src-tauri/* +!/src-tauri/tauri.conf.json .vscode *.yaml diff --git a/apps/halidoscope/src-tauri/Cargo.lock b/apps/halidoscope/src-tauri/Cargo.lock index 7d78f17b5c77..6bfd545bc73f 100644 --- a/apps/halidoscope/src-tauri/Cargo.lock +++ b/apps/halidoscope/src-tauri/Cargo.lock @@ -17,6 +17,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -97,6 +115,38 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -263,6 +313,49 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + [[package]] name = "base64" version = "0.21.7" @@ -290,6 +383,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -305,6 +404,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -366,6 +474,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + [[package]] name = "bumpalo" version = "3.20.3" @@ -384,6 +498,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -467,6 +587,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -542,6 +664,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.5" @@ -650,12 +778,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -924,6 +1077,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "embed-resource" version = "3.0.9" @@ -971,6 +1130,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1019,12 +1198,33 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -1343,6 +1543,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gio" version = "0.18.4" @@ -1491,11 +1701,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "halidoscope" version = "0.1.0" dependencies = [ "colorous", + "image", "serde", "serde_json", "tauri", @@ -1790,6 +2012,46 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" + [[package]] name = "indexmap" version = "1.9.3" @@ -1822,6 +2084,17 @@ dependencies = [ "cfb", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1853,6 +2126,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1926,6 +2208,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.99" @@ -1977,6 +2269,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -2016,6 +2314,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2062,6 +2370,15 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "markup5ever" version = "0.38.0" @@ -2073,6 +2390,16 @@ dependencies = [ "web_atoms", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.1" @@ -2115,6 +2442,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.19.2" @@ -2166,12 +2503,77 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2492,6 +2894,18 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -2648,6 +3062,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -2726,6 +3149,46 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.39.4" @@ -2756,12 +3219,111 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2865,6 +3427,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3185,6 +3753,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "1.0.3" @@ -3748,6 +4325,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -4192,6 +4783,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "version-compare" version = "0.2.1" @@ -4471,6 +5073,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -5021,6 +5629,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.2" @@ -5105,6 +5719,26 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -5165,6 +5799,30 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.12.0" diff --git a/apps/halidoscope/src-tauri/Cargo.toml b/apps/halidoscope/src-tauri/Cargo.toml index d436f7a2af86..110ee19e7258 100644 --- a/apps/halidoscope/src-tauri/Cargo.toml +++ b/apps/halidoscope/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" colorous = "1.0.16" +image = "0.25.10" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-cli = "2.0.0" diff --git a/apps/halidoscope/src-tauri/src/cli.rs b/apps/halidoscope/src-tauri/src/cli.rs new file mode 100644 index 000000000000..f15a000425b5 --- /dev/null +++ b/apps/halidoscope/src-tauri/src/cli.rs @@ -0,0 +1,165 @@ +use image; +use tauri_plugin_cli::SubcommandMatches; + +use crate::render::{ + GrayscaleState, LoadFrequencyState, RedundantState, Renderer, ReuseDistanceState, RgbState, + StoreFrequencyState, +}; +use crate::trace::{func_extents, Trace}; + +pub fn halidoscope_cli(subcommand: Box) { + match subcommand.name.as_str() { + "snapshot" => { + let args = &subcommand.matches.args; + + let trace = args + .get("trace") + .and_then(|a| a.value.as_str()) + .unwrap_or_else(|| { + eprintln!("Error: --trace argument is required."); + std::process::exit(1); + }); + let func = args + .get("func") + .and_then(|a| a.value.as_str()) + .unwrap_or_else(|| { + eprintln!("Error: --func argument is required."); + std::process::exit(1); + }); + let packet_index = args + .get("packet-index") + .and_then(|a| a.value.as_str()) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + let mode = args + .get("mode") + .and_then(|a| a.value.as_str()) + .unwrap_or("grayscale"); + let destination = args + .get("destination") + .and_then(|a| a.value.as_str()) + .unwrap_or_else(|| { + eprintln!("Error: A destination for the snapshot is required."); + std::process::exit(1); + }); + + // Load and parse the trace. + let tr = Trace::load_from_file(trace).unwrap_or_else(|e| { + eprintln!("Error loading trace: {}", e); + std::process::exit(1); + }); + + // Find the target function. + let target_func = tr.funcs.get(func).unwrap_or_else(|| { + eprintln!( + "Func '{}' not found in trace. Available Funcs: {:?}", + func, + tr.funcs.keys().collect::>() + ); + std::process::exit(1); + }); + + // Exit early if the packet index is out of bounds. + if packet_index as usize >= tr.packets.len() { + eprintln!( + "Packet index {} is out of bounds. Valid range: 0..{}", + packet_index, + tr.packets.len() + ); + std::process::exit(1); + } + + let store_indices = tr.func_store_indices(func).unwrap_or(&[]); + let load_indices = tr.func_load_indices(func).unwrap_or(&[]); + + let buffer = match mode { + "grayscale" => { + write_buffer::(&tr, func, store_indices, packet_index) + } + "rgb" => write_buffer::(&tr, func, store_indices, packet_index), + "store-frequency" => { + write_buffer::(&tr, func, store_indices, packet_index) + } + "load-frequency" => { + write_buffer::(&tr, func, load_indices, packet_index) + } + "redundant-stores" => { + write_buffer::(&tr, func, store_indices, packet_index) + } + "reuse-distance" => write_reuse_distance_buffer( + &tr, + func, + store_indices, + load_indices, + packet_index, + ), + _ => { + eprintln!("Unknown rendering mode: {}", mode); + std::process::exit(1); + } + }; + + if let Some((width, height, _, _)) = func_extents(target_func) { + match image::save_buffer( + &destination, + &buffer, + width as u32, + height as u32, + image::ColorType::Rgba8, + ) { + Ok(_) => { + println!("Snapshot written to {}", destination); + std::process::exit(0); + } + Err(e) => { + eprintln!("Error saving snapshot: {}", e); + std::process::exit(1); + } + } + } + + // If we reach here, it means we couldn't determine the dimensions of the Func. + eprintln!( + "Could not determine dimensions for Func '{}'. Ensure it has valid geometry.", + func + ); + std::process::exit(1); + } + cmd => { + eprintln!("Unknown subcommand {}", cmd); + std::process::exit(1); + } + } +} + +fn write_buffer( + trace: &Trace, + func: &str, + indices: &[usize], + packet_index: u32, +) -> Vec { + if let Some(mut state) = R::register(trace, func) { + let k = indices.partition_point(|&p| p <= packet_index as usize); + state.seek(trace, indices, k); + state.to_rgba() + } else { + Vec::new() + } +} + +fn write_reuse_distance_buffer( + trace: &Trace, + func: &str, + store_indices: &[usize], + load_indices: &[usize], + packet_index: u32, +) -> Vec { + if let Some(mut state) = ReuseDistanceState::new(trace, func) { + let store_k = store_indices.partition_point(|&p| p <= packet_index as usize); + let load_k = load_indices.partition_point(|&p| p <= packet_index as usize); + state.seek(trace, store_indices, load_indices, store_k, load_k); + state.to_rgba() + } else { + Vec::new() + } +} diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 7e2c694a189e..d53c6b74b6c2 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -10,7 +10,7 @@ use tauri::ipc::Response; use tauri::State; use crate::render::{ - GrayscaleState, LoadFrequencyState, RedundantState, ReuseDistanceState, RgbState, + GrayscaleState, LoadFrequencyState, RedundantState, Renderer, ReuseDistanceState, RgbState, StoreFrequencyState, }; use crate::trace::Trace; diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index d4a21d6ed9a4..c88ff5a6eb99 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -1,8 +1,12 @@ +use tauri_plugin_cli::CliExt; + +use crate::cli::halidoscope_cli; + +pub mod cli; pub mod commands; pub mod render; pub mod trace; -// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn get_cwd() -> Result { std::env::current_dir() @@ -17,9 +21,18 @@ fn get_cwd() -> Result { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_cli::init()) .setup(|app| { - #[cfg(desktop)] - app.handle().plugin(tauri_plugin_cli::init())?; + match app.cli().matches() { + Ok(matches) => match matches.subcommand { + Some(subcommand) => halidoscope_cli(subcommand), + None => {} + }, + Err(e) => { + eprintln!("Error parsing CLI arguments: {}", e); + std::process::exit(1); + } + } Ok(()) }) .plugin(tauri_plugin_opener::init()) diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index f7f3a370bb11..27266c9041c5 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -8,6 +8,13 @@ use ::colorous; use crate::trace::{pixel_xy, FuncGeometry, Trace, TracePacket}; +// A trait that all rendering states implement. +pub trait Renderer: Sized { + fn register(trace: &Trace, func: &str) -> Option; + fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize); + fn to_rgba(&self) -> Vec; +} + // ── Grayscale rendering ─────────────────────────────────────────────────────── /// Accumulated pixel state for a single Func. Channel 0 is normalized to [0, 255] and replicated @@ -38,18 +45,6 @@ impl GrayscaleState { }) } - pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { - let target_k = target_k.min(store_indices.len()); - if target_k < self.applied_k { - self.framebuffer.iter_mut().for_each(|b| *b = 0); - self.applied_k = 0; - } - for &global_idx in &store_indices[self.applied_k..target_k] { - self.apply_store(&trace.packets[global_idx]); - } - self.applied_k = target_k; - } - fn apply_store(&mut self, pkt: &TracePacket) { let n_lanes = pkt.type_.lanes.max(1) as usize; let dims_per_lane = pkt.coordinates.len() / n_lanes; @@ -85,15 +80,29 @@ impl GrayscaleState { #[inline] fn normalize(&self, v: f64) -> u8 { - if self.max_v > self.min_v { - (255.0 * (v - self.min_v) / (self.max_v - self.min_v)).clamp(0.0, 255.0) as u8 - } else { - 128 + (255.0 * (v - self.min_v) / (self.max_v - self.min_v)).clamp(0.0, 255.0) as u8 + } +} + +impl Renderer for GrayscaleState { + fn register(trace: &Trace, func: &str) -> Option { + Self::new(trace, func) + } + + fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + if target_k < self.applied_k { + self.framebuffer.iter_mut().for_each(|b| *b = 0); + self.applied_k = 0; + } + for &global_idx in &store_indices[self.applied_k..target_k] { + self.apply_store(&trace.packets[global_idx]); } + self.applied_k = target_k; } /// Produces a `width * height * 4` RGBA8 buffer. Channel 0 is replicated across R/G/B. - pub fn to_rgba(&self) -> Vec { + fn to_rgba(&self) -> Vec { let FuncGeometry { width, height, @@ -122,10 +131,6 @@ impl GrayscaleState { } out } - - pub fn channels(&self) -> usize { - self.geom.channels - } } // ── RGB rendering ───────────────────────────────────────────────────────────── @@ -158,18 +163,6 @@ impl RgbState { }) } - pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { - let target_k = target_k.min(store_indices.len()); - if target_k < self.applied_k { - self.framebuffer.iter_mut().for_each(|b| *b = 0); - self.applied_k = 0; - } - for &global_idx in &store_indices[self.applied_k..target_k] { - self.apply_store(&trace.packets[global_idx]); - } - self.applied_k = target_k; - } - fn apply_store(&mut self, pkt: &TracePacket) { let n_lanes = pkt.type_.lanes.max(1) as usize; let dims_per_lane = pkt.coordinates.len() / n_lanes; @@ -207,10 +200,28 @@ impl RgbState { fn normalize(&self, v: f64) -> u8 { (255.0 * (v - self.min_v) / (self.max_v - self.min_v)).clamp(0.0, 255.0) as u8 } +} + +impl Renderer for RgbState { + fn register(trace: &Trace, func: &str) -> Option { + Self::new(trace, func) + } + + fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + if target_k < self.applied_k { + self.framebuffer.iter_mut().for_each(|b| *b = 0); + self.applied_k = 0; + } + for &global_idx in &store_indices[self.applied_k..target_k] { + self.apply_store(&trace.packets[global_idx]); + } + self.applied_k = target_k; + } /// Produces a `width * height * 4` RGBA8 buffer. Planes 0/1/2 map to R/G/B; /// missing planes are 0. Alpha is always opaque. - pub fn to_rgba(&self) -> Vec { + fn to_rgba(&self) -> Vec { let FuncGeometry { width, height, @@ -236,10 +247,6 @@ impl RgbState { } out } - - pub fn channels(&self) -> usize { - self.geom.channels - } } // ── Store frequency rendering ───────────────────────────────────────────────── @@ -272,18 +279,6 @@ impl StoreFrequencyState { }) } - pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { - let target_k = target_k.min(store_indices.len()); - if target_k < self.applied_k { - self.counts.iter_mut().for_each(|c| *c = 0); - self.applied_k = 0; - } - for &idx in &store_indices[self.applied_k..target_k] { - self.increment_pixel(&trace.packets[idx]); - } - self.applied_k = target_k; - } - fn increment_pixel(&mut self, pkt: &TracePacket) { let FuncGeometry { width, @@ -301,11 +296,29 @@ impl StoreFrequencyState { } } } +} + +impl Renderer for StoreFrequencyState { + fn register(trace: &Trace, func: &str) -> Option { + Self::new(trace, func) + } + + fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + if target_k < self.applied_k { + self.counts.iter_mut().for_each(|c| *c = 0); + self.applied_k = 0; + } + for &idx in &store_indices[self.applied_k..target_k] { + self.increment_pixel(&trace.packets[idx]); + } + self.applied_k = target_k; + } /// Produces a `width × height × 4` RGBA8 buffer with the Inferno colormap applied. Counts /// are normalized against the global full-trace maximum so intensities are comparable across /// all Funcs. - pub fn to_rgba(&self) -> Vec { + fn to_rgba(&self) -> Vec { let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { @@ -359,18 +372,6 @@ impl LoadFrequencyState { }) } - pub fn seek(&mut self, trace: &Trace, load_indices: &[usize], target_k: usize) { - let target_k = target_k.min(load_indices.len()); - if target_k < self.applied_k { - self.counts.iter_mut().for_each(|c| *c = 0); - self.applied_k = 0; - } - for &idx in &load_indices[self.applied_k..target_k] { - self.increment_pixel(&trace.packets[idx]); - } - self.applied_k = target_k; - } - fn increment_pixel(&mut self, pkt: &TracePacket) { let FuncGeometry { width, @@ -388,11 +389,29 @@ impl LoadFrequencyState { } } } +} + +impl Renderer for LoadFrequencyState { + fn register(trace: &Trace, func: &str) -> Option { + Self::new(trace, func) + } + + fn seek(&mut self, trace: &Trace, load_indices: &[usize], target_k: usize) { + let target_k = target_k.min(load_indices.len()); + if target_k < self.applied_k { + self.counts.iter_mut().for_each(|c| *c = 0); + self.applied_k = 0; + } + for &idx in &load_indices[self.applied_k..target_k] { + self.increment_pixel(&trace.packets[idx]); + } + self.applied_k = target_k; + } /// Produces a `width × height × 4` RGBA8 buffer with the Inferno colormap applied. Counts /// are normalized against the global full-trace maximum so intensities are comparable across /// all Funcs. - pub fn to_rgba(&self) -> Vec { + fn to_rgba(&self) -> Vec { let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { @@ -461,19 +480,6 @@ impl RedundantState { self.applied_k = 0; } - /// Seeks to the state after the first `target_k` store events. Forward seeks apply only the - /// delta; backward seeks reset and replay from zero. - pub fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { - let target_k = target_k.min(store_indices.len()); - if target_k < self.applied_k { - self.reset(); - } - for &global_idx in &store_indices[self.applied_k..target_k] { - self.apply_store(&trace.packets[global_idx]); - } - self.applied_k = target_k; - } - fn apply_store(&mut self, pkt: &TracePacket) { let n_lanes = pkt.type_.lanes.max(1) as usize; let dims_per_lane = pkt.coordinates.len() / n_lanes; @@ -514,11 +520,30 @@ impl RedundantState { self.last_values[val_idx] = Some(v_bits); } } +} + +impl Renderer for RedundantState { + fn register(trace: &Trace, func: &str) -> Option { + Self::new(trace, func) + } + + /// Seeks to the state after the first `target_k` store events. Forward seeks apply only the + /// delta; backward seeks reset and replay from zero. + fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + if target_k < self.applied_k { + self.reset(); + } + for &global_idx in &store_indices[self.applied_k..target_k] { + self.apply_store(&trace.packets[global_idx]); + } + self.applied_k = target_k; + } /// Produces a `width × height × 4` RGBA8 buffer. Pixels with zero redundant stores are black; /// pixels with one or more are mapped through the Inferno colormap, normalized against the /// global full-trace maximum so intensities are comparable across all Funcs. - pub fn to_rgba(&self) -> Vec { + fn to_rgba(&self) -> Vec { let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index fb5588eb53ad..ba90c47b309f 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -408,6 +408,7 @@ fn parse_func_type_and_dim( // path is "tag seeds, accesses expand." if !min_coords.is_empty() { let entry = funcs.entry(qualified.to_owned()).or_default(); + entry.name = qualified.to_owned(); entry.min_coords = min_coords; entry.max_coords = max_coords; } @@ -548,7 +549,8 @@ impl Trace { parse_func_type_and_dim(&qualified, &trace_tag, &mut funcs); } EventCode::BeginRealization => { - funcs.entry(qualified.clone()).or_default(); + let entry = funcs.entry(qualified.clone()).or_default(); + entry.name = qualified.clone(); // Start the liveness range for this Func at the current packet index. let idx = packets.len() as u32; @@ -577,7 +579,10 @@ impl Trace { // Add the load event to the list of pending loads to support DAG inference. pending_loads.push((func_name.clone(), parent_id)); - let stats = funcs.entry(qualified.clone()).or_default(); + let stats = funcs.entry(qualified.clone()).or_insert_with(|| FuncStats { + name: qualified.clone(), + ..Default::default() + }); // Update the min/max coordinate and value ranges for this Func based on the // current load packet. @@ -595,7 +600,10 @@ impl Trace { // Update the min/max coordinate and value ranges for this Func based on the // current store packet. - let stats = funcs.entry(qualified.clone()).or_default(); + let stats = funcs.entry(qualified.clone()).or_insert_with(|| FuncStats { + name: qualified.clone(), + ..Default::default() + }); update_coord_range(&pkt, stats); update_value_range(&pkt, stats); } @@ -1046,7 +1054,7 @@ fn count_histogram(counts: &[i32]) -> (i32, Vec) { /// Returns `(width, height, min_x, min_y)` for a Func, or `None` if the stats /// have no coordinate information or produce a zero-area extent. -fn func_extents(stats: &FuncStats) -> Option<(usize, usize, i32, i32)> { +pub fn func_extents(stats: &FuncStats) -> Option<(usize, usize, i32, i32)> { if stats.min_coords.is_empty() || stats.max_coords.is_empty() { return None; } diff --git a/apps/halidoscope/src-tauri/tauri.conf.json b/apps/halidoscope/src-tauri/tauri.conf.json index ebba2a7451cc..3c776ee652d2 100644 --- a/apps/halidoscope/src-tauri/tauri.conf.json +++ b/apps/halidoscope/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Halidoscope", "version": "0.1.0", - "identifier": "com.halide.halidescope", + "identifier": "com.halide.halidoscope", "build": { "beforeDevCommand": "pnpm dev", "devUrl": "http://localhost:1420", @@ -39,9 +39,56 @@ "name": "trace", "short": "t", "takesValue": true, - "description": "Path to .hltrace file to load on startup" + "description": "Path to .hltrace file to load on startup." } - ] + ], + "subcommands": { + "snapshot": { + "args": [ + { + "name": "trace", + "short": "t", + "takesValue": true, + "description": "Path to .hltrace file to load for snapshotting.", + "required": true + }, + { + "name": "func", + "short": "f", + "takesValue": true, + "description": "Name of the Func to snapshot.", + "required": true + }, + { + "name": "packet-index", + "short": "i", + "takesValue": true, + "description": "Global packet index to snapshot." + }, + { + "name": "mode", + "short": "m", + "takesValue": true, + "possibleValues": [ + "grayscale", + "rgb", + "store-frequency", + "load-frequency", + "redundant-stores", + "reuse-distance" + ], + "description": "Rendering mode for the snapshot." + }, + { + "name": "destination", + "index": 1, + "takesValue": true, + "description": "Path to write the output snapshot image.", + "required": true + } + ] + } + } } } } From 9443c30673af9f83817d01d58365fa60dc484c74 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Mon, 6 Jul 2026 14:30:07 -0700 Subject: [PATCH 16/67] Add dot subcommand to Halidoscope CLI. --- apps/halidoscope/src-tauri/src/cli.rs | 51 +++++++ apps/halidoscope/src-tauri/src/graph.rs | 16 +++ apps/halidoscope/src-tauri/src/lib.rs | 1 + apps/halidoscope/src-tauri/src/trace.rs | 157 ++++++++------------- apps/halidoscope/src-tauri/tauri.conf.json | 17 +++ 5 files changed, 147 insertions(+), 95 deletions(-) create mode 100644 apps/halidoscope/src-tauri/src/graph.rs diff --git a/apps/halidoscope/src-tauri/src/cli.rs b/apps/halidoscope/src-tauri/src/cli.rs index f15a000425b5..c92f40d26feb 100644 --- a/apps/halidoscope/src-tauri/src/cli.rs +++ b/apps/halidoscope/src-tauri/src/cli.rs @@ -1,6 +1,10 @@ +use std::ffi::OsStr; +use std::path::Path; + use image; use tauri_plugin_cli::SubcommandMatches; +use crate::graph::to_dot; use crate::render::{ GrayscaleState, LoadFrequencyState, RedundantState, Renderer, ReuseDistanceState, RgbState, StoreFrequencyState, @@ -125,6 +129,53 @@ pub fn halidoscope_cli(subcommand: Box) { ); std::process::exit(1); } + "dot" => { + let args = &subcommand.matches.args; + + let trace = args + .get("trace") + .and_then(|a| a.value.as_str()) + .unwrap_or_else(|| { + eprintln!("Error: --trace argument is required."); + std::process::exit(1); + }); + let destination = args.get("destination").and_then(|a| a.value.as_str()); + + // Load and parse the trace. + let tr = Trace::load_from_file(trace).unwrap_or_else(|e| { + eprintln!("Error loading trace: {}", e); + std::process::exit(1); + }); + let dot = to_dot(&tr.dag_edges); + + match destination { + Some(dest) => { + let ext = Path::new(dest).extension().and_then(OsStr::to_str); + + match ext { + Some("txt") | Some("gv") | Some("dot") => { + if let Err(e) = std::fs::write(dest, &dot) { + eprintln!("Failed to write DOT file: {}", e); + std::process::exit(1); + } + + println!("DOT file written to {}", dest); + std::process::exit(0); + } + _ => { + eprintln!( + "Unsupported file extension for DOT file, must be one of .txt, .gv, or .dot." + ); + std::process::exit(1); + } + } + } + None => { + println!("{}", dot); + std::process::exit(0); + } + } + } cmd => { eprintln!("Unknown subcommand {}", cmd); std::process::exit(1); diff --git a/apps/halidoscope/src-tauri/src/graph.rs b/apps/halidoscope/src-tauri/src/graph.rs new file mode 100644 index 000000000000..44b2670246d7 --- /dev/null +++ b/apps/halidoscope/src-tauri/src/graph.rs @@ -0,0 +1,16 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write; + +pub fn to_dot(dag_edges: &BTreeMap>) -> String { + let mut dot = String::from("strict digraph {\n\trankdir = LR\n\n"); + + for (key, value) in dag_edges.iter() { + for dest in value { + write!(dot, "\t{key} -> {dest}\n").unwrap_or_default(); + } + } + + write!(dot, "}}").unwrap_or_default(); + + dot +} diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index c88ff5a6eb99..8d49242e8cc9 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ use crate::cli::halidoscope_cli; pub mod cli; pub mod commands; +pub mod graph; pub mod render; pub mod trace; diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index ba90c47b309f..03ecf88fa616 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -230,7 +230,6 @@ pub struct FuncGeometry { pub struct Trace { pub packets: Vec, pub funcs: BTreeMap, - pub pipelines: BTreeMap, pub dag_edges: BTreeMap>, pub store_indices_by_func: BTreeMap>, pub load_indices_by_func: BTreeMap>, @@ -356,7 +355,7 @@ fn update_value_range(pkt: &TracePacket, stats: &mut FuncStats) { // ── func_type_and_dim tag parsing ───────────────────────────────────────────── fn parse_func_type_and_dim( - qualified: &str, + func_name: &str, trace_tag: &str, funcs: &mut BTreeMap, ) { @@ -407,8 +406,8 @@ fn parse_func_type_and_dim( // In practice Halide emits this tag at pipeline start, before any load/store, so the common // path is "tag seeds, accesses expand." if !min_coords.is_empty() { - let entry = funcs.entry(qualified.to_owned()).or_default(); - entry.name = qualified.to_owned(); + let entry = funcs.entry(func_name.to_owned()).or_default(); + entry.name = func_name.to_owned(); entry.min_coords = min_coords; entry.max_coords = max_coords; } @@ -428,7 +427,6 @@ impl Trace { let mut packets: Vec = Vec::new(); let mut funcs: BTreeMap = BTreeMap::new(); - let mut pipelines: BTreeMap = BTreeMap::new(); let mut dag_edges: BTreeMap> = BTreeMap::new(); let mut store_indices_by_func: BTreeMap> = BTreeMap::new(); let mut load_indices_by_func: BTreeMap> = BTreeMap::new(); @@ -436,11 +434,7 @@ impl Trace { let mut produce_ranges_by_func: BTreeMap> = BTreeMap::new(); let mut consume_ranges_by_func: BTreeMap> = BTreeMap::new(); - // id -> pipeline name: propagated down the parent chain so every event in a pipeline can - // compute its qualified name. - let mut parent_to_pipeline: HashMap = HashMap::new(); - - // id -> (event, qualified_name, parent_id): needed for DAG inference after all packets are + // id -> (event, func_name, parent_id): needed for DAG inference after all packets are // parsed. let mut id_to_info: HashMap = HashMap::new(); @@ -502,30 +496,7 @@ impl Trace { .unwrap_or_default(); // ── Pipeline context propagation ───────────────────────────────────────────────────── - match ev { - EventCode::BeginPipeline => { - pipelines.insert(id, func_name.clone()); - parent_to_pipeline.insert(id, func_name.clone()); - } - EventCode::EndPipeline => { - parent_to_pipeline.remove(&parent_id); - } - _ => { - // Propagate the pipeline name down the parent chain so every event can compute - // the pipeline it belongs to. - if let Some(pl) = parent_to_pipeline.get(&parent_id).cloned() { - parent_to_pipeline.insert(id, pl); - } - } - } - - // ── Qualified name ─────────────────────────────────────────────────────────────────── - let qualified = match parent_to_pipeline.get(&parent_id) { - Some(pl) if !pl.is_empty() => format!("{}:{}", pl, func_name), - _ => func_name.clone(), - }; - - id_to_info.insert(id, (ev, qualified.clone(), parent_id)); + id_to_info.insert(id, (ev, func_name.clone(), parent_id)); // ── Build the packet ───────────────────────────────────────────────────────────────── let pkt = TracePacket { @@ -546,17 +517,17 @@ impl Trace { // max_coords; their interaction is order-dependent by design. match ev { EventCode::Tag if trace_tag.starts_with("func_type_and_dim:") => { - parse_func_type_and_dim(&qualified, &trace_tag, &mut funcs); + parse_func_type_and_dim(&func_name, &trace_tag, &mut funcs); } EventCode::BeginRealization => { - let entry = funcs.entry(qualified.clone()).or_default(); - entry.name = qualified.clone(); + let entry = funcs.entry(func_name.clone()).or_default(); + entry.name = func_name.clone(); // Start the liveness range for this Func at the current packet index. let idx = packets.len() as u32; buffer_liveness_range_by_func - .entry(qualified.clone()) + .entry(func_name.clone()) .and_modify(|range| range.0 = range.0.min(idx)) .or_insert((idx, idx)); } @@ -565,7 +536,7 @@ impl Trace { let idx = packets.len() as u32; buffer_liveness_range_by_func - .entry(qualified.clone()) + .entry(func_name.clone()) .and_modify(|range| range.1 = range.1.max(idx)) .or_insert((idx, idx)); } @@ -573,14 +544,14 @@ impl Trace { // When we observe a load event, add its current index (equivalent to // packets.len() before the push) to our BTreeMap of load indices for this Func. load_indices_by_func - .entry(qualified.clone()) + .entry(func_name.clone()) .or_default() .push(packets.len()); // Add the load event to the list of pending loads to support DAG inference. pending_loads.push((func_name.clone(), parent_id)); - let stats = funcs.entry(qualified.clone()).or_insert_with(|| FuncStats { - name: qualified.clone(), + let stats = funcs.entry(func_name.clone()).or_insert_with(|| FuncStats { + name: func_name.clone(), ..Default::default() }); @@ -594,14 +565,14 @@ impl Trace { // packets.len() before the push) to our BTreeMap of store indices for this // Func. store_indices_by_func - .entry(qualified.clone()) + .entry(func_name.clone()) .or_default() .push(packets.len()); // Update the min/max coordinate and value ranges for this Func based on the // current store packet. - let stats = funcs.entry(qualified.clone()).or_insert_with(|| FuncStats { - name: qualified.clone(), + let stats = funcs.entry(func_name.clone()).or_insert_with(|| FuncStats { + name: func_name.clone(), ..Default::default() }); update_coord_range(&pkt, stats); @@ -611,14 +582,14 @@ impl Trace { let idx = packets.len() as u32; produce_ranges_by_func - .entry(qualified.clone()) + .entry(func_name.clone()) .or_default() .push((idx, idx)); } EventCode::EndProduce => { let idx = packets.len() as u32; - if let Some(ranges) = produce_ranges_by_func.get_mut(qualified.as_str()) { + if let Some(ranges) = produce_ranges_by_func.get_mut(func_name.as_str()) { if let Some(last) = ranges.last_mut() { last.1 = idx; } @@ -628,14 +599,14 @@ impl Trace { let idx = packets.len() as u32; consume_ranges_by_func - .entry(qualified.clone()) + .entry(func_name.clone()) .or_default() .push((idx, idx)); } EventCode::EndConsume => { let idx = packets.len() as u32; - if let Some(ranges) = consume_ranges_by_func.get_mut(qualified.as_str()) { + if let Some(ranges) = consume_ranges_by_func.get_mut(func_name.as_str()) { if let Some(last) = ranges.last_mut() { last.1 = idx; } @@ -652,10 +623,7 @@ impl Trace { // Walk up the parent chain from each load to find the enclosing Produce event; that // Produce's func is a producer of the loaded func. for (func_name, load_parent_id) in &pending_loads { - let loaded_func = match parent_to_pipeline.get(load_parent_id) { - Some(pl) if !pl.is_empty() => format!("{}:{}", pl, func_name), - _ => func_name.clone(), - }; + let loaded_func = func_name.clone(); let mut current = *load_parent_id; loop { @@ -675,11 +643,11 @@ impl Trace { } } - // Compute max per-pixel store/load counts for each Func using the qualified-name index - // lists. We extract extents first (shared borrow) then write back (mut borrow) to keep the - // two borrows of `funcs` non-overlapping. - for (qualified, indices) in &store_indices_by_func { - let extents = funcs.get(qualified.as_str()).and_then(func_extents); + // Compute max per-pixel store/load counts for each Func using the index lists. We extract + // extents first (shared borrow) then write back (mut borrow) to keep the two borrows of + // `funcs` non-overlapping. + for (func_name, indices) in &store_indices_by_func { + let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { let mut counts = vec![0i32; w * h]; for &idx in indices { @@ -693,7 +661,7 @@ impl Trace { } } } - if let Some(stats) = funcs.get_mut(qualified.as_str()) { + if let Some(stats) = funcs.get_mut(func_name.as_str()) { let (max, hist) = count_histogram(&counts); stats.max_store_count = max; stats.store_count_histogram = hist; @@ -701,8 +669,8 @@ impl Trace { } } - for (qualified, indices) in &load_indices_by_func { - let extents = funcs.get(qualified.as_str()).and_then(func_extents); + for (func_name, indices) in &load_indices_by_func { + let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { let mut counts = vec![0i32; w * h]; for &idx in indices { @@ -716,7 +684,7 @@ impl Trace { } } } - if let Some(stats) = funcs.get_mut(qualified.as_str()) { + if let Some(stats) = funcs.get_mut(func_name.as_str()) { let (max, hist) = count_histogram(&counts); stats.max_load_count = max; stats.load_count_histogram = hist; @@ -727,10 +695,10 @@ impl Trace { // Compute max per-pixel redundant store counts: replay all stores for each Func, tracking // the last value written to each (x, y, channel). A store is redundant when the incoming // value bit-matches the previously stored value at that location. - for (qualified, indices) in &store_indices_by_func { - let extents = funcs.get(qualified.as_str()).and_then(func_extents); + for (func_name, indices) in &store_indices_by_func { + let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { - let stats = funcs.get(qualified.as_str()).unwrap(); + let stats = funcs.get(func_name.as_str()).unwrap(); let (channels, min_c) = if stats.min_coords.len() >= 3 { ( (stats.max_coords[2] - stats.min_coords[2]).max(1) as usize, @@ -773,7 +741,7 @@ impl Trace { last_values[val_idx] = Some(v_bits); } } - if let Some(stats) = funcs.get_mut(qualified.as_str()) { + if let Some(stats) = funcs.get_mut(func_name.as_str()) { let (max, hist) = count_histogram(&redundant_counts); stats.max_redundant_count = max; stats.redundant_count_histogram = hist; @@ -795,10 +763,10 @@ impl Trace { // Per-pixel distance vecs are collected here; histogram building is deferred until the // global max is known so all Funcs share the same bucket scale. let mut reuse_distances_by_func: BTreeMap> = BTreeMap::new(); - for (qualified, store_indices) in &store_indices_by_func { - let extents = funcs.get(qualified.as_str()).and_then(func_extents); + for (func_name, store_indices) in &store_indices_by_func { + let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { - let stats = funcs.get(qualified.as_str()).unwrap(); + let stats = funcs.get(func_name.as_str()).unwrap(); let (channels, min_c) = if stats.min_coords.len() >= 3 { ( (stats.max_coords[2] - stats.min_coords[2]).max(1) as usize, @@ -808,7 +776,7 @@ impl Trace { (1, 0) }; let load_indices = load_indices_by_func - .get(qualified.as_str()) + .get(func_name.as_str()) .map(Vec::as_slice) .unwrap_or(&[]); @@ -875,23 +843,23 @@ impl Trace { } } - if let Some(stats) = funcs.get_mut(qualified.as_str()) { + if let Some(stats) = funcs.get_mut(func_name.as_str()) { stats.max_reuse_distance = max_reuse_distances.iter().copied().max().unwrap_or(0); } - reuse_distances_by_func.insert(qualified.clone(), max_reuse_distances); + reuse_distances_by_func.insert(func_name.clone(), max_reuse_distances); } } // Pipeline inputs: Funcs with loads but no stores. The first load at each (x, y, channel) // is free (analogous to a memcpy). Subsequent loads measure distance from that first load. - for (qualified, load_indices) in &load_indices_by_func { - if store_indices_by_func.contains_key(qualified.as_str()) { + for (func_name, load_indices) in &load_indices_by_func { + if store_indices_by_func.contains_key(func_name.as_str()) { continue; // handled by the store-anchor loop above } - let extents = funcs.get(qualified.as_str()).and_then(func_extents); + let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { - let stats = funcs.get(qualified.as_str()).unwrap(); + let stats = funcs.get(func_name.as_str()).unwrap(); let (channels, min_c) = if stats.min_coords.len() >= 3 { ( (stats.max_coords[2] - stats.min_coords[2]).max(1) as usize, @@ -935,11 +903,11 @@ impl Trace { } } - if let Some(stats) = funcs.get_mut(qualified.as_str()) { + if let Some(stats) = funcs.get_mut(func_name.as_str()) { stats.max_reuse_distance = max_reuse_distances.iter().copied().max().unwrap_or(0); } - reuse_distances_by_func.insert(qualified.clone(), max_reuse_distances); + reuse_distances_by_func.insert(func_name.clone(), max_reuse_distances); } } @@ -951,7 +919,7 @@ impl Trace { .max() .unwrap_or(0); if global_max_reuse_distance > 0 { - for (qualified, distances) in &reuse_distances_by_func { + for (func_name, distances) in &reuse_distances_by_func { let mut hist = vec![0u32; 64]; for &dist in distances { if dist > 0 { @@ -960,7 +928,7 @@ impl Trace { hist[bucket] += 1; } } - if let Some(stats) = funcs.get_mut(qualified.as_str()) { + if let Some(stats) = funcs.get_mut(func_name.as_str()) { stats.reuse_distance_histogram = hist; } } @@ -969,7 +937,6 @@ impl Trace { Ok(Self { packets, funcs, - pipelines, dag_edges, store_indices_by_func, load_indices_by_func, @@ -981,38 +948,38 @@ impl Trace { // ── Render-path accessors ───────────────────────────────────────────────── - /// Global packet indices of `qualified`'s store events, in ascending order. `None` if the Func + /// Global packet indices of `func_name`'s store events, in ascending order. `None` if the Func /// emitted no stores. Use `partition_point(|&p| p <= g)` on the returned slice to turn a global /// timeline index `g` into the number of stores that have occurred by that point. - pub fn func_store_indices(&self, qualified: &str) -> Option<&[usize]> { - self.store_indices_by_func.get(qualified).map(Vec::as_slice) + pub fn func_store_indices(&self, func_name: &str) -> Option<&[usize]> { + self.store_indices_by_func.get(func_name).map(Vec::as_slice) } - pub fn func_load_indices(&self, qualified: &str) -> Option<&[usize]> { - self.load_indices_by_func.get(qualified).map(Vec::as_slice) + pub fn func_load_indices(&self, func_name: &str) -> Option<&[usize]> { + self.load_indices_by_func.get(func_name).map(Vec::as_slice) } - pub fn func_buffer_liveness_range(&self, qualified: &str) -> Option<&(u32, u32)> { - self.buffer_liveness_range_by_func.get(qualified) + pub fn func_buffer_liveness_range(&self, func_name: &str) -> Option<&(u32, u32)> { + self.buffer_liveness_range_by_func.get(func_name) } - pub fn func_produce_ranges(&self, qualified: &str) -> Option<&[(u32, u32)]> { + pub fn func_produce_ranges(&self, func_name: &str) -> Option<&[(u32, u32)]> { self.produce_ranges_by_func - .get(qualified) + .get(func_name) .map(Vec::as_slice) } - pub fn func_consume_ranges(&self, qualified: &str) -> Option<&[(u32, u32)]> { + pub fn func_consume_ranges(&self, func_name: &str) -> Option<&[(u32, u32)]> { self.consume_ranges_by_func - .get(qualified) + .get(func_name) .map(Vec::as_slice) } - /// Spatial layout for `qualified`, or `None` if it has no usable coordinate extent. Reuses + /// Spatial layout for `func_name`, or `None` if it has no usable coordinate extent. Reuses /// `func_extents` for pixel dims so the renderer and the metadata layer agree, and adds the /// channel axis (logical dim 2). - pub fn func_geometry(&self, qualified: &str) -> Option { - let stats = self.funcs.get(qualified)?; + pub fn func_geometry(&self, func_name: &str) -> Option { + let stats = self.funcs.get(func_name)?; let (width, height, min_x, min_y) = func_extents(stats)?; let (channels, min_c) = if stats.min_coords.len() >= 3 { ( diff --git a/apps/halidoscope/src-tauri/tauri.conf.json b/apps/halidoscope/src-tauri/tauri.conf.json index 3c776ee652d2..b767a788f080 100644 --- a/apps/halidoscope/src-tauri/tauri.conf.json +++ b/apps/halidoscope/src-tauri/tauri.conf.json @@ -87,6 +87,23 @@ "required": true } ] + }, + "dot": { + "args": [ + { + "name": "trace", + "short": "t", + "takesValue": true, + "description": "Path to .hltrace file to analyze for pipeline graph structure.", + "required": true + }, + { + "name": "destination", + "index": 1, + "takesValue": true, + "description": "Path to write the output snapshot image." + } + ] } } } From 8f41c43e723722a444ccdd364180d50cec05b467 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 7 Jul 2026 12:11:28 -0700 Subject: [PATCH 17/67] Adjust packet parsing to accommodate #9192 and report thread count information by Func. --- apps/halidoscope/src-tauri/src/commands.rs | 8 +- apps/halidoscope/src-tauri/src/trace.rs | 124 ++++++++++++++---- .../src/components/controls/FuncsPanel.tsx | 6 + apps/halidoscope/src/types/index.ts | 1 + 4 files changed, 111 insertions(+), 28 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index d53c6b74b6c2..d21725f6ebcf 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -2,7 +2,7 @@ //! //! This module owns the types that cross the Tauri IPC boundary. -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::sync::Mutex; use serde::Serialize; @@ -51,6 +51,7 @@ pub struct FuncMeta { pub buffer_liveness: IndexRange, pub produce_ranges: Vec, pub consume_ranges: Vec, + pub thread_count: u32, } /// Top-level payload returned by `open_trace`. @@ -131,6 +132,11 @@ impl TraceMeta { .copied() .map(IndexRange::from_tuple) .collect(), + thread_count: (trace + .func_thread_ids(name) + .unwrap_or(&BTreeSet::new()) + .len() as u32) + .max(1), } }) .collect(); diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 03ecf88fa616..77d9b5f6fb5b 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -61,6 +61,8 @@ pub enum EventCode { BeginPipeline, EndPipeline, Tag, + BeginParallelTask, + EndParallelTask, Unknown(i32), } @@ -78,6 +80,8 @@ impl EventCode { 8 => Self::BeginPipeline, 9 => Self::EndPipeline, 10 => Self::Tag, + 11 => Self::BeginParallelTask, + 12 => Self::EndParallelTask, other => Self::Unknown(other), } } @@ -87,11 +91,19 @@ impl EventCode { #[derive(Debug, Clone)] pub struct TracePacket { + /// This packet's own id, for the purpose of other packets' `parent_id`. Only meaningful for + /// non-load/store events; load/store packets are leaves (nothing parents against them) and + /// carry `value_index` in this slot instead, so `id` is meaningless for them. pub id: i32, pub event: EventCode, pub parent_id: i32, + /// Which tuple element was accessed. Only meaningful for load/store events. pub value_index: i32, + /// Only meaningful for load/store events. pub type_: HalideType, + /// The Halide-internal thread that executed a parallel task. Only meaningful for + /// `BeginParallelTask`; zero for every other event. + pub thread_id: i32, /// Coordinates in dim-major / lane-minor order: [x₀..xₙ, y₀..yₙ, c₀..cₙ] where n = type_.lanes. pub coordinates: Vec, pub value: Vec, @@ -236,27 +248,25 @@ pub struct Trace { pub buffer_liveness_range_by_func: BTreeMap, pub produce_ranges_by_func: BTreeMap>, pub consume_ranges_by_func: BTreeMap>, + pub thread_ids_by_func: BTreeMap>, } // ── Binary parsing helpers ──────────────────────────────────────────────────── -// halide_trace_packet_t fixed header: 7 × 4 bytes = 28 bytes. -// u32 size @ 0 -// i32 id @ 4 -// u8 type.code @ 8 -// u8 type.bits @ 9 -// u16 type.lanes @ 10 -// i32 event @ 12 -// i32 parent_id @ 16 -// i32 value_index @ 20 -// i32 dimensions @ 24 +// halide_trace_packet_t fixed header: 6 × 4 bytes = 24 bytes. +// u32 size @ 0 +// i32 event @ 4 +// i32 parent_id @ 8 +// union { i32 id; i32 value_index; } @ 12 +// union { type{code, bits, lanes}; i32 thread_id; } @ 16 +// i32 dimensions @ 20 // // Immediately after the header: // i32 coordinates[dimensions] // u8 value[type.lanes * ceil(type.bits / 8)] // char func[] (null-terminated) // char trace_tag[] (null-terminated; empty string if absent) -const HEADER_BYTES: usize = 28; +const HEADER_BYTES: usize = 24; // Helper functions to read little-endian integers from a byte buffer at a given offset. try_into() // will convert the slice to a fixed-size [u8; N] array. We inline these for performance since they @@ -433,6 +443,7 @@ impl Trace { let mut buffer_liveness_range_by_func: BTreeMap = BTreeMap::new(); let mut produce_ranges_by_func: BTreeMap> = BTreeMap::new(); let mut consume_ranges_by_func: BTreeMap> = BTreeMap::new(); + let mut thread_ids_by_func: BTreeMap> = BTreeMap::new(); // id -> (event, func_name, parent_id): needed for DAG inference after all packets are // parsed. @@ -441,6 +452,9 @@ impl Trace { // Loads we deferred for DAG inference. let mut pending_loads: Vec<(String, i32)> = Vec::new(); + // Parallel task starts we deferred for accumulating thread IDs by Func. + let mut pending_parallel_tasks: Vec<(i32, i32)> = Vec::new(); + // Packet parsing loop. while pos + HEADER_BYTES <= total { let size = u32_le(data, pos) as usize; @@ -449,27 +463,52 @@ impl Trace { } // ── Fixed header fields ─────────────────────────────────────────── - let id = i32_le(data, pos + 4); - let type_code = data[pos + 8]; - let type_bits = data[pos + 9]; - let type_lanes = u16_le(data, pos + 10); - let event = i32_le(data, pos + 12); - let parent_id = i32_le(data, pos + 16); - let value_index = i32_le(data, pos + 20); - let dimensions = i32_le(data, pos + 24) as usize; - - let type_ = HalideType { - code: TypeCode::from_u8(type_code), - bits: type_bits, - lanes: type_lanes, - }; + let event = i32_le(data, pos + 4); + let parent_id = i32_le(data, pos + 8); + let dimensions = i32_le(data, pos + 20) as usize; + let ev = EventCode::from_i32(event); + let is_load_or_store = matches!(ev, EventCode::Load | EventCode::Store); + + // Slot @ 12 is `id` for non-load/store events and `value_index` for load/store + // events; slot @ 16 is `type` for load/store events and `thread_id` (else 0) + // otherwise. See halide_trace_packet_t in HalideRuntime.h. + let (id, value_index) = if is_load_or_store { + (0, i32_le(data, pos + 12)) + } else { + (i32_le(data, pos + 12), 0) + }; + + let (type_, thread_id) = if is_load_or_store { + let type_ = HalideType { + code: TypeCode::from_u8(data[pos + 16]), + bits: data[pos + 17], + lanes: u16_le(data, pos + 18), + }; + (type_, 0) + } else { + ( + HalideType { + code: TypeCode::from_u8(0), + bits: 0, + lanes: 0, + }, + i32_le(data, pos + 16), + ) + }; + let pkt_data = &data[pos..pos + size]; // ── Variable-length trailing fields ─────────────────────────────── let coords_off = HEADER_BYTES; let value_off = coords_off + dimensions * 4; - let value_len = type_.value_bytes(); + // Only load/store packets have a value; the type/thread_id slot isn't a real + // halide_type_t for other events, so value_bytes() must not be trusted for them. + let value_len = if is_load_or_store { + type_.value_bytes() + } else { + 0 + }; let func_off = value_off + value_len; let coords: Vec = (0..dimensions) @@ -496,7 +535,12 @@ impl Trace { .unwrap_or_default(); // ── Pipeline context propagation ───────────────────────────────────────────────────── - id_to_info.insert(id, (ev, func_name.clone(), parent_id)); + // Load/store packets don't carry a real `id` (that slot holds `value_index` + // instead) and are leaves that nothing ever parents against, so they're excluded + // here to avoid a `value_index` colliding with and clobbering a real packet's entry. + if !is_load_or_store { + id_to_info.insert(id, (ev, func_name.clone(), parent_id)); + } // ── Build the packet ───────────────────────────────────────────────────────────────── let pkt = TracePacket { @@ -505,6 +549,7 @@ impl Trace { parent_id, value_index, type_, + thread_id, coordinates: coords, value, func: func_name.clone(), @@ -612,6 +657,9 @@ impl Trace { } } } + EventCode::BeginParallelTask => { + pending_parallel_tasks.push((thread_id, parent_id)); + } _ => {} } @@ -643,6 +691,23 @@ impl Trace { } } + for (thread_id, parent_id) in &pending_parallel_tasks { + let mut current = *parent_id; + loop { + match id_to_info.get(¤t) { + Some((EventCode::Produce, producing_func, _)) => { + thread_ids_by_func + .entry(producing_func.clone()) + .or_default() + .insert(*thread_id); + break; + } + Some((_, _, next_parent)) => current = *next_parent, + None => break, + } + } + } + // Compute max per-pixel store/load counts for each Func using the index lists. We extract // extents first (shared borrow) then write back (mut borrow) to keep the two borrows of // `funcs` non-overlapping. @@ -943,6 +1008,7 @@ impl Trace { buffer_liveness_range_by_func, produce_ranges_by_func, consume_ranges_by_func, + thread_ids_by_func, }) } @@ -975,6 +1041,10 @@ impl Trace { .map(Vec::as_slice) } + pub fn func_thread_ids(&self, func_name: &str) -> Option<&BTreeSet> { + self.thread_ids_by_func.get(func_name) + } + /// Spatial layout for `func_name`, or `None` if it has no usable coordinate extent. Reuses /// `func_extents` for pixel dims so the renderer and the metadata layer agree, and adds the /// channel axis (logical dim 2). diff --git a/apps/halidoscope/src/components/controls/FuncsPanel.tsx b/apps/halidoscope/src/components/controls/FuncsPanel.tsx index c49c74ed3dea..664ef92701d8 100644 --- a/apps/halidoscope/src/components/controls/FuncsPanel.tsx +++ b/apps/halidoscope/src/components/controls/FuncsPanel.tsx @@ -75,6 +75,12 @@ function FuncsPanel({ funcs }: FuncsPanelProps) { {func.max_load_count.toLocaleString()} + + Thread Count + + + {func.thread_count.toLocaleString()} +
diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts index aa5c46754eae..1b814812572e 100644 --- a/apps/halidoscope/src/types/index.ts +++ b/apps/halidoscope/src/types/index.ts @@ -30,6 +30,7 @@ export interface FuncMeta extends Record { buffer_liveness: IndexRange; produce_ranges: IndexRange[]; consume_ranges: IndexRange[]; + thread_count: number; } /** Top-level payload returned by `open_trace`. Mirrors the Rust `TraceMeta`. */ From d15673082c980adbb6297f58b7ce9ee7114ff101 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 7 Jul 2026 12:12:26 -0700 Subject: [PATCH 18/67] Fix responsive text aliasing issues by using NodeToolbar. --- apps/halidoscope/src/App.css | 5 ----- .../components/views/tracer/FuncCanvas.tsx | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/halidoscope/src/App.css b/apps/halidoscope/src/App.css index ada85e03319b..2fd65a1a864e 100644 --- a/apps/halidoscope/src/App.css +++ b/apps/halidoscope/src/App.css @@ -48,11 +48,6 @@ input[type="number"] { } @layer components { - .text-responsive { - font-size: min(calc(0.75rem / var(--zoom-level)), 0.75rem); - line-height: min(calc(1rem / var(--zoom-level)), 1rem); - } - @keyframes slideDown { from { height: 0; diff --git a/apps/halidoscope/src/components/views/tracer/FuncCanvas.tsx b/apps/halidoscope/src/components/views/tracer/FuncCanvas.tsx index 98550c3f50d2..d9a3959e6194 100644 --- a/apps/halidoscope/src/components/views/tracer/FuncCanvas.tsx +++ b/apps/halidoscope/src/components/views/tracer/FuncCanvas.tsx @@ -4,6 +4,7 @@ import { Handle, type Node, type NodeProps, + NodeToolbar, Position, useEdges, useNodes, @@ -134,10 +135,21 @@ function FuncCanvas({ data }: NodeProps) { }, [packetIndex, name, width, height, renderMode]); return ( -
- - {name} - + <> + + = 0.5 && zoom < 1.5, + "text-sm": zoom >= 1.5, + }, + )} + > + {name} + +
) { ) : null} -
+ ); } From 939f78355b2566d6e98860ee67048dbbaa0cd2bc Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Wed, 8 Jul 2026 14:35:14 -0700 Subject: [PATCH 19/67] Fix liveness producer-consumer derivation to align more closely with the notion of producer-consumer in the original Halide paper. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/src-tauri/src/trace.rs | 13 +- apps/halidoscope/src/App.css | 3 +- .../src/components/canvas/Canvas.tsx | 113 ++++++++++++++++ .../src/components/canvas/FuncEdge.tsx | 76 +++++++++++ .../FuncCanvas.tsx => canvas/FuncNode.tsx} | 77 +++++++---- .../{shared => canvas}/HandleCircle.tsx | 0 .../src/components/canvas/Overlay.tsx | 20 +++ .../controls/graph/GraphDisplay.tsx | 17 +-- .../controls/liveness/LivenessControls.tsx | 101 +++++++------- .../src/components/icons/CheckIcon.tsx | 21 +++ .../src/components/shared/Canvas.tsx | 126 ------------------ .../src/components/views/tracer/Tracer.tsx | 4 +- apps/halidoscope/src/state/liveness.ts | 7 +- apps/halidoscope/src/types/index.ts | 3 +- apps/halidoscope/src/utils/graph.ts | 21 ++- apps/halidoscope/src/utils/liveness.ts | 12 ++ 16 files changed, 381 insertions(+), 233 deletions(-) create mode 100644 apps/halidoscope/src/components/canvas/Canvas.tsx create mode 100644 apps/halidoscope/src/components/canvas/FuncEdge.tsx rename apps/halidoscope/src/components/{views/tracer/FuncCanvas.tsx => canvas/FuncNode.tsx} (75%) rename apps/halidoscope/src/components/{shared => canvas}/HandleCircle.tsx (100%) create mode 100644 apps/halidoscope/src/components/canvas/Overlay.tsx create mode 100644 apps/halidoscope/src/components/icons/CheckIcon.tsx delete mode 100644 apps/halidoscope/src/components/shared/Canvas.tsx diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 77d9b5f6fb5b..e4fe8e050352 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -626,7 +626,9 @@ impl Trace { EventCode::Produce => { let idx = packets.len() as u32; - produce_ranges_by_func + // When we observe a Produce event for a Func A, this signals that A is + // consuming some other Func B. Thus, we store this as A's consume range. + consume_ranges_by_func .entry(func_name.clone()) .or_default() .push((idx, idx)); @@ -634,7 +636,7 @@ impl Trace { EventCode::EndProduce => { let idx = packets.len() as u32; - if let Some(ranges) = produce_ranges_by_func.get_mut(func_name.as_str()) { + if let Some(ranges) = consume_ranges_by_func.get_mut(func_name.as_str()) { if let Some(last) = ranges.last_mut() { last.1 = idx; } @@ -643,7 +645,10 @@ impl Trace { EventCode::Consume => { let idx = packets.len() as u32; - consume_ranges_by_func + // When we observe a Consume event for a Func A, this signals that A is + // producing for (being consumed by) some other Func B. Thus, we store this as + // A's produce range. + produce_ranges_by_func .entry(func_name.clone()) .or_default() .push((idx, idx)); @@ -651,7 +656,7 @@ impl Trace { EventCode::EndConsume => { let idx = packets.len() as u32; - if let Some(ranges) = consume_ranges_by_func.get_mut(func_name.as_str()) { + if let Some(ranges) = produce_ranges_by_func.get_mut(func_name.as_str()) { if let Some(last) = ranges.last_mut() { last.1 = idx; } diff --git a/apps/halidoscope/src/App.css b/apps/halidoscope/src/App.css index 2fd65a1a864e..185b4e5139d0 100644 --- a/apps/halidoscope/src/App.css +++ b/apps/halidoscope/src/App.css @@ -40,9 +40,10 @@ input[type="number"] { --color-ps-border-primary: oklch(0.3407 0 0); --color-ps-border-secondary: oklch(0.3979 0 0); --color-ps-border-tertiary: oklch(0.4997 0 0); - --color-highlight: oklch(0.77 0.1919 163.7); + --color-realization: oklch(0.77 0.1919 163.7); --color-produce: oklch(0.837 0.14 75); --color-consume: oklch(0.74 0.175 305.4); + --text-tiny: 0.625rem; --text-tiny--line-height: 1.5; } diff --git a/apps/halidoscope/src/components/canvas/Canvas.tsx b/apps/halidoscope/src/components/canvas/Canvas.tsx new file mode 100644 index 000000000000..629352eab355 --- /dev/null +++ b/apps/halidoscope/src/components/canvas/Canvas.tsx @@ -0,0 +1,113 @@ +import { + applyEdgeChanges, + ReactFlow, + useNodesState, + useViewport, + type Node, + type Edge, + type EdgeChange, +} from "@xyflow/react"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import * as React from "react"; + +import FuncEdge from "@/components/canvas/FuncEdge"; +import FuncNode from "@/components/canvas/FuncNode"; +import Overlay from "@/components/canvas/Overlay"; +import { funcAtom } from "@/state/func"; +import { edgesAtom } from "@/state/graph"; +import { livenessAtom } from "@/state/liveness"; +import { FuncMeta } from "@/types"; +import { buildEdges, buildNodes, getLayoutedElements } from "@/utils/graph"; + +const NODE_TYPES = { + funcNode: FuncNode, +}; + +const EDGE_TYPES = { + funcEdge: FuncEdge, +}; + +interface CanvasProps { + funcs: Record; + dagEdges: Record; +} + +function Canvas({ funcs, dagEdges }: CanvasProps) { + const { nodes: initialNodes, edges: initialEdges } = React.useMemo(() => { + return getLayoutedElements( + buildNodes(funcs, "funcNode"), + buildEdges(dagEdges, "funcEdge"), + ); + }, [funcs, dagEdges]); + const [nodes, _setNodes, onNodesChange] = + useNodesState>(initialNodes); + const [edges, setEdges] = useAtom(edgesAtom); + const setFunc = useSetAtom(funcAtom); + const liveness = useAtomValue(livenessAtom); + const { zoom } = useViewport(); + + React.useEffect(() => { + setEdges(initialEdges); + }, [initialEdges, setEdges]); + + const onEdgesChange = React.useCallback( + (changes: EdgeChange[]) => { + setEdges((eds) => applyEdgeChanges(changes, eds)); + }, + [setEdges], + ); + + React.useEffect(() => { + document.documentElement.style.setProperty("--zoom-level", zoom.toString()); + }, [zoom]); + + return ( +
+ setFunc(node.data.name)} + /> + {liveness.active ? ( + + {liveness.mode === "realizations" ? ( +
+
+
+
+ Func Buffer Live in Memory +
+ ) : ( +
+
+
+
+
+ Func Producing +
+
+
+
+
+ Func Consuming +
+
+ )} + + ) : null} + + Zoom: {Math.round(zoom * 100)}% + +
+ ); +} + +export default Canvas; diff --git a/apps/halidoscope/src/components/canvas/FuncEdge.tsx b/apps/halidoscope/src/components/canvas/FuncEdge.tsx new file mode 100644 index 000000000000..1457c27f26de --- /dev/null +++ b/apps/halidoscope/src/components/canvas/FuncEdge.tsx @@ -0,0 +1,76 @@ +import { BaseEdge, getBezierPath, type EdgeProps } from "@xyflow/react"; +import { useAtomValue } from "jotai"; +import * as React from "react"; + +import { useTraceContext } from "@/hooks/trace"; +import { livenessAtom } from "@/state/liveness"; +import { packetAtom } from "@/state/packet"; +import { isEdgeLive } from "@/utils/liveness"; + +function FuncEdge({ + id, + source, + target, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style, + markerStart, + markerEnd, +}: EdgeProps) { + const [path] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + const gradientId = `produce-consume-gradient-${id}`; + + const { funcs } = useTraceContext(); + const liveness = useAtomValue(livenessAtom); + const packetIndex = useAtomValue(packetAtom); + const isProduceConsumeMode = React.useMemo(() => { + return ( + liveness.active && + liveness.mode === "produce-consume" && + isEdgeLive(funcs, source, target, packetIndex) + ); + }, [liveness, funcs, packetIndex, source, target]); + + return ( + <> + + + + + + + + + ); +} + +export default FuncEdge; diff --git a/apps/halidoscope/src/components/views/tracer/FuncCanvas.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx similarity index 75% rename from apps/halidoscope/src/components/views/tracer/FuncCanvas.tsx rename to apps/halidoscope/src/components/canvas/FuncNode.tsx index d9a3959e6194..53fcfb05bf4d 100644 --- a/apps/halidoscope/src/components/views/tracer/FuncCanvas.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -10,11 +10,12 @@ import { useNodes, useViewport, } from "@xyflow/react"; -import clsx from "clsx"; +import { clsx } from "clsx"; import { useAtomValue } from "jotai"; import * as React from "react"; -import HandleCircle from "@/components/shared/HandleCircle"; +import HandleCircle from "@/components/canvas/HandleCircle"; +import { useTraceContext } from "@/hooks/trace"; import { livenessAtom } from "@/state/liveness"; import { packetAtom } from "@/state/packet"; import { renderModeAtom } from "@/state/render"; @@ -27,40 +28,52 @@ import { renderRedundantStores, renderReuseDistance, } from "@/utils/api"; -import { - isFuncBufferLive, - isFuncConsuming, - isFuncProducing, -} from "@/utils/liveness"; - -type FuncNode = Node; +import { isFuncBufferLive, isEdgeLive } from "@/utils/liveness"; -function FuncCanvas({ data }: NodeProps) { +function FuncNode({ data }: NodeProps>) { const { name, width, height } = data; const canvasRef = React.useRef(null); - const livenessMode = useAtomValue(livenessAtom); + const { funcs } = useTraceContext(); + const liveness = useAtomValue(livenessAtom); const packetIndex = useAtomValue(packetAtom); const renderMode = useAtomValue(renderModeAtom); + const nodes = useNodes(); + const edges = useEdges(); + const bufferLive = React.useMemo( () => - livenessMode === "realizations" && isFuncBufferLive(data, packetIndex), - [livenessMode, data, packetIndex], + liveness.active && + liveness.mode === "realizations" && + isFuncBufferLive(data, packetIndex), + [liveness, data, packetIndex], ); + const producing = React.useMemo( () => - livenessMode === "produce-consume" && isFuncProducing(data, packetIndex), - [livenessMode, data, packetIndex], + liveness.active && + liveness.mode === "produce-consume" && + edges.some( + (edge) => + edge.source === name && + isEdgeLive(funcs, edge.source, edge.target, packetIndex), + ), + [liveness, edges, funcs, name, packetIndex], ); + const consuming = React.useMemo( () => - livenessMode === "produce-consume" && isFuncConsuming(data, packetIndex), - [livenessMode, data, packetIndex], + liveness.active && + liveness.mode === "produce-consume" && + edges.some( + (edge) => + edge.target === name && + isEdgeLive(funcs, edge.source, edge.target, packetIndex), + ), + [liveness, edges, funcs, name, packetIndex], ); - const nodes = useNodes(); - const edges = useEdges(); const incomingEdgeCount = React.useMemo( () => getIncomers({ id: name }, nodes, edges).length, [name, nodes, edges], @@ -105,12 +118,12 @@ function FuncCanvas({ data }: NodeProps) { case "Load Frequency": buffer = await renderLoadFrequency(name, target); break; - case "Reuse Distance": - buffer = await renderReuseDistance(name, target); - break; case "Redundant Stores": buffer = await renderRedundantStores(name, target); break; + case "Reuse Distance": + buffer = await renderReuseDistance(name, target); + break; } const ctx = canvasRef.current?.getContext("2d"); @@ -136,14 +149,20 @@ function FuncCanvas({ data }: NodeProps) { return ( <> - + = 0.5 && zoom < 1.5, - "text-sm": zoom >= 1.5, + "text-xs": zoom >= 0.5, }, )} > @@ -152,7 +171,7 @@ function FuncCanvas({ data }: NodeProps) {
) { width={width} height={height} className={clsx("ring-transparent", { - "ring-highlight!": bufferLive, + "ring-realization!": bufferLive, "ring-produce!": producing, "ring-consume!": consuming, "ring-2": zoom < 1, @@ -196,4 +215,4 @@ function FuncCanvas({ data }: NodeProps) { ); } -export default FuncCanvas; +export default FuncNode; diff --git a/apps/halidoscope/src/components/shared/HandleCircle.tsx b/apps/halidoscope/src/components/canvas/HandleCircle.tsx similarity index 100% rename from apps/halidoscope/src/components/shared/HandleCircle.tsx rename to apps/halidoscope/src/components/canvas/HandleCircle.tsx diff --git a/apps/halidoscope/src/components/canvas/Overlay.tsx b/apps/halidoscope/src/components/canvas/Overlay.tsx new file mode 100644 index 000000000000..0956f5294609 --- /dev/null +++ b/apps/halidoscope/src/components/canvas/Overlay.tsx @@ -0,0 +1,20 @@ +import { clsx } from "clsx"; +import type * as React from "react"; + +function Overlay({ + children, + className, +}: React.PropsWithChildren<{ className: string }>) { + return ( +
+ {children} +
+ ); +} + +export default Overlay; diff --git a/apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx b/apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx index df367658cddb..1a445f04d7f6 100644 --- a/apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx +++ b/apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx @@ -3,6 +3,7 @@ import { useSetAtom } from "jotai"; import { Checkbox } from "radix-ui"; import * as React from "react"; +import CheckIcon from "@/components/icons/CheckIcon"; import { edgesAtom } from "@/state/graph"; function hideEdge(hidden: boolean) { @@ -31,21 +32,7 @@ function GraphDisplay() { onCheckedChange={onEdgeVisibilityChange} > - - - + diff --git a/apps/halidoscope/src/components/controls/liveness/LivenessControls.tsx b/apps/halidoscope/src/components/controls/liveness/LivenessControls.tsx index 52a264d6f658..a6f832945a0a 100644 --- a/apps/halidoscope/src/components/controls/liveness/LivenessControls.tsx +++ b/apps/halidoscope/src/components/controls/liveness/LivenessControls.tsx @@ -1,60 +1,69 @@ import { useAtom } from "jotai"; -import { RadioGroup } from "radix-ui"; +import { Checkbox, RadioGroup } from "radix-ui"; +import CheckIcon from "@/components/icons/CheckIcon"; import { livenessAtom, type LivenessMode } from "@/state/liveness"; function LivenessControls() { - const [livenessMode, setLivenessMode] = useAtom(livenessAtom); + const [liveness, setLiveness] = useAtom(livenessAtom); return ( - setLivenessMode(value as LivenessMode)} - > +
- { + setLiveness({ ...liveness, active: !!checked }); + }} > - - - + + + + +
-
- - - - -
-
- - - - -
- +
+ + + + +
+
+ + + + +
+ + ) : null} +
); } diff --git a/apps/halidoscope/src/components/icons/CheckIcon.tsx b/apps/halidoscope/src/components/icons/CheckIcon.tsx new file mode 100644 index 000000000000..bbdcf7dd0ee2 --- /dev/null +++ b/apps/halidoscope/src/components/icons/CheckIcon.tsx @@ -0,0 +1,21 @@ +function CheckIcon() { + return ( + + + + ); +} + +export default CheckIcon; diff --git a/apps/halidoscope/src/components/shared/Canvas.tsx b/apps/halidoscope/src/components/shared/Canvas.tsx deleted file mode 100644 index e02466a15a00..000000000000 --- a/apps/halidoscope/src/components/shared/Canvas.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - applyEdgeChanges, - ReactFlow, - useNodesState, - useViewport, - type Node, - type Edge, - type EdgeChange, -} from "@xyflow/react"; -import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import * as React from "react"; - -import FuncCanvas from "@/components/views/tracer/FuncCanvas"; -import { funcAtom } from "@/state/func"; -import { edgesAtom } from "@/state/graph"; -import { livenessAtom } from "@/state/liveness"; -import { packetAtom } from "@/state/packet"; -import { FuncMeta, NodeTypes } from "@/types"; -import { buildEdges, buildNodes, getLayoutedElements } from "@/utils/graph"; -import { isFuncConsuming, isFuncProducing } from "@/utils/liveness"; - -const NODE_TYPES = { - funcCanvas: FuncCanvas, -}; - -interface CanvasProps { - funcs: Record; - dagEdges: Record; - type: NodeTypes; -} - -function Canvas({ funcs, dagEdges, type }: CanvasProps) { - const { nodes: initialNodes, edges: initialEdges } = React.useMemo(() => { - return getLayoutedElements(buildNodes(funcs, type), buildEdges(dagEdges)); - }, [funcs, dagEdges, type]); - - const [nodes, _setNodes, onNodesChange] = - useNodesState>(initialNodes); - const [edges, setEdges] = useAtom(edgesAtom); - const setFunc = useSetAtom(funcAtom); - - React.useEffect(() => { - setEdges(initialEdges); - }, [initialEdges, setEdges]); - - const onEdgesChange = React.useCallback( - (changes: EdgeChange[]) => { - setEdges((eds) => applyEdgeChanges(changes, eds)); - }, - [setEdges], - ); - - const livenessMode = useAtomValue(livenessAtom); - const packetIndex = useAtomValue(packetAtom); - - const consumingFuncs = React.useMemo(() => { - if (livenessMode !== "produce-consume") { - return new Set(); - } - - const fs = new Set(); - for (const [funcName, func] of Object.entries(funcs)) { - if (isFuncConsuming(func, packetIndex)) { - fs.add(funcName); - } - } - - return fs; - }, [livenessMode, funcs, packetIndex]); - - const producingFuncs = React.useMemo(() => { - if (livenessMode !== "produce-consume") { - return new Set(); - } - - const fs = new Set(); - for (const [funcName, func] of Object.entries(funcs)) { - if (isFuncProducing(func, packetIndex)) { - fs.add(funcName); - } - } - - return fs; - }, [livenessMode, funcs, packetIndex]); - - const styledEdges = React.useMemo(() => { - return edges.map((edge) => { - if (consumingFuncs.has(edge.source) && producingFuncs.has(edge.target)) { - return { - ...edge, - style: { stroke: "var(--color-produce)" }, - }; - } - - return edge; - }); - }, [edges, consumingFuncs, producingFuncs]); - - const { zoom } = useViewport(); - - React.useEffect(() => { - document.documentElement.style.setProperty("--zoom-level", zoom.toString()); - }, [zoom]); - - return ( -
- setFunc(node.data.name)} - /> -
- Zoom: {Math.round(zoom * 100)}% -
-
- ); -} - -export default Canvas; diff --git a/apps/halidoscope/src/components/views/tracer/Tracer.tsx b/apps/halidoscope/src/components/views/tracer/Tracer.tsx index 885cc49ea326..5526eb5e146a 100644 --- a/apps/halidoscope/src/components/views/tracer/Tracer.tsx +++ b/apps/halidoscope/src/components/views/tracer/Tracer.tsx @@ -1,6 +1,6 @@ import { ReactFlowProvider } from "@xyflow/react"; -import Canvas from "@/components/shared/Canvas"; +import Canvas from "@/components/canvas/Canvas"; import Timeline from "@/components/views/tracer/TracerTimeline"; import { useTraceContext } from "@/hooks/trace"; import ControlTabs from "@/components/controls/ControlTabs"; @@ -14,7 +14,7 @@ function Tracer() { {Object.keys(funcs).length > 0 ? ( <> - + diff --git a/apps/halidoscope/src/state/liveness.ts b/apps/halidoscope/src/state/liveness.ts index 539d67c05f1f..a15c6c81b83e 100644 --- a/apps/halidoscope/src/state/liveness.ts +++ b/apps/halidoscope/src/state/liveness.ts @@ -1,5 +1,8 @@ import { atom } from "jotai"; -export type LivenessMode = "none" | "realizations" | "produce-consume"; +export type LivenessMode = "realizations" | "produce-consume"; -export const livenessAtom = atom("none"); +export const livenessAtom = atom<{ active: boolean; mode: LivenessMode }>({ + active: false, + mode: "realizations", +}); diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts index 1b814812572e..b0a7e73ee913 100644 --- a/apps/halidoscope/src/types/index.ts +++ b/apps/halidoscope/src/types/index.ts @@ -44,4 +44,5 @@ export interface TraceMeta { global_max_reuse_distance: number; } -export type NodeTypes = "funcCanvas"; +export type NodeTypes = "funcNode"; +export type EdgeTypes = "funcEdge"; diff --git a/apps/halidoscope/src/utils/graph.ts b/apps/halidoscope/src/utils/graph.ts index f30f3e4f3b18..0d8554b39d89 100644 --- a/apps/halidoscope/src/utils/graph.ts +++ b/apps/halidoscope/src/utils/graph.ts @@ -1,12 +1,13 @@ import Dagre from "@dagrejs/dagre"; import type { Node, Edge } from "@xyflow/react"; -import { FuncMeta, NodeTypes } from "../types"; +import { EdgeTypes, FuncMeta, NodeTypes } from "@/types"; /** * Build xyflow nodes from the backend's funcs payload, which maps Halide func - * @param funcs - * @returns + * @param funcs The funcs payload from the backend. + * @param type The node type to assign to each node. + * @returns An array of nodes formatted for use with @xyflow/react. */ export function buildNodes( funcs: Record, @@ -15,7 +16,7 @@ export function buildNodes( return Object.entries(funcs).map(([name, stats]) => { return { id: name, - type: type, + type, position: { x: 0, y: 0, @@ -34,19 +35,25 @@ export function buildNodes( * consumers to their producers. * * @param dagEdges The dag_edges payload from the backend. - * @returns An array of edges formatted for use with xyflow, where each edge has an id, source, and target. + * @param type The edge type to assign to each edge. + * @returns An array of edges formatted for use with @xyflow/react. */ -export function buildEdges(dagEdges: Record): Edge[] { +export function buildEdges( + dagEdges: Record, + type: EdgeTypes, +): Edge[] { const edges: { id: string; source: string; target: string; + type: EdgeTypes; }[] = []; for (const [producer, consumers] of Object.entries(dagEdges)) { for (const consumer of consumers) { edges.push({ id: `${producer}-${consumer}`, + type, source: producer, target: consumer, }); @@ -61,7 +68,7 @@ export function getLayoutedElements( edges: Edge[], ): { nodes: Node[]; edges: Edge[] } { const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); - g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: 80 }); + g.setGraph({ rankdir: "LR", nodesep: 60, ranksep: 80 }); edges.forEach((edge) => g.setEdge(edge.source, edge.target)); nodes.forEach((node) => { diff --git a/apps/halidoscope/src/utils/liveness.ts b/apps/halidoscope/src/utils/liveness.ts index 402c10241451..687a0ebc251c 100644 --- a/apps/halidoscope/src/utils/liveness.ts +++ b/apps/halidoscope/src/utils/liveness.ts @@ -18,3 +18,15 @@ export function isFuncProducing(func: FuncMeta, globalIndex: number) { (range) => range.start <= globalIndex && globalIndex <= range.end, ); } + +export function isEdgeLive( + funcs: Record, + source: string, + target: string, + globalIndex: number, +) { + return ( + isFuncProducing(funcs[source], globalIndex) && + isFuncConsuming(funcs[target], globalIndex) + ); +} From 8241df79f6c8f272bfdbd2600cdc80677a4438f3 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 9 Jul 2026 11:24:55 -0700 Subject: [PATCH 20/67] Adjust liveness color mappings. --- apps/halidoscope/src/App.css | 4 ++-- apps/halidoscope/src/components/canvas/Canvas.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/halidoscope/src/App.css b/apps/halidoscope/src/App.css index 185b4e5139d0..523ec934d815 100644 --- a/apps/halidoscope/src/App.css +++ b/apps/halidoscope/src/App.css @@ -40,8 +40,8 @@ input[type="number"] { --color-ps-border-primary: oklch(0.3407 0 0); --color-ps-border-secondary: oklch(0.3979 0 0); --color-ps-border-tertiary: oklch(0.4997 0 0); - --color-realization: oklch(0.77 0.1919 163.7); - --color-produce: oklch(0.837 0.14 75); + --color-realization: oklch(0.837 0.14 75); + --color-produce: oklch(0.77 0.1919 163.7); --color-consume: oklch(0.74 0.175 305.4); --text-tiny: 0.625rem; diff --git a/apps/halidoscope/src/components/canvas/Canvas.tsx b/apps/halidoscope/src/components/canvas/Canvas.tsx index 629352eab355..d2dee94821ae 100644 --- a/apps/halidoscope/src/components/canvas/Canvas.tsx +++ b/apps/halidoscope/src/components/canvas/Canvas.tsx @@ -83,7 +83,7 @@ function Canvas({ funcs, dagEdges }: CanvasProps) {
- Func Buffer Live in Memory + Buffer Live in Memory
) : (
@@ -91,13 +91,13 @@ function Canvas({ funcs, dagEdges }: CanvasProps) {
- Func Producing + Producer
- Func Consuming + Consumer
)} From 872367a530893bd1276aa4954792e85be9c65b86 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 9 Jul 2026 15:29:31 -0700 Subject: [PATCH 21/67] Adjust redundant store count metric to mark a redundant store only if there are no intervening loads. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/src-tauri/src/trace.rs | 281 ++++++++++++++---------- 1 file changed, 168 insertions(+), 113 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index e4fe8e050352..92a0fa7a1cbe 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -696,6 +696,7 @@ impl Trace { } } + // Compute the set of thread IDs used to compute each Func. for (thread_id, parent_id) in &pending_parallel_tasks { let mut current = *parent_id; loop { @@ -722,14 +723,17 @@ impl Trace { let mut counts = vec![0i32; w * h]; for &idx in indices { let pkt = &packets[idx]; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for l in 0..n_lanes { - let (x, y) = pixel_xy(pkt, l, n_lanes, dims_per_lane, min_x, min_y); - if x >= 0 && y >= 0 && (x as usize) < w && (y as usize) < h { - counts[y as usize * w + x as usize] += 1; - } - } + for_each_lane_pixel( + pkt, + min_x, + min_y, + w, + h, + None, + |_lane, pixel_idx, _val_idx| { + counts[pixel_idx] += 1; + }, + ); } if let Some(stats) = funcs.get_mut(func_name.as_str()) { let (max, hist) = count_histogram(&counts); @@ -745,14 +749,17 @@ impl Trace { let mut counts = vec![0i32; w * h]; for &idx in indices { let pkt = &packets[idx]; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for l in 0..n_lanes { - let (x, y) = pixel_xy(pkt, l, n_lanes, dims_per_lane, min_x, min_y); - if x >= 0 && y >= 0 && (x as usize) < w && (y as usize) < h { - counts[y as usize * w + x as usize] += 1; - } - } + for_each_lane_pixel( + pkt, + min_x, + min_y, + w, + h, + None, + |_lane, pixel_idx, _val_idx| { + counts[pixel_idx] += 1; + }, + ); } if let Some(stats) = funcs.get_mut(func_name.as_str()) { let (max, hist) = count_histogram(&counts); @@ -764,8 +771,9 @@ impl Trace { // Compute max per-pixel redundant store counts: replay all stores for each Func, tracking // the last value written to each (x, y, channel). A store is redundant when the incoming - // value bit-matches the previously stored value at that location. - for (func_name, indices) in &store_indices_by_func { + // value bit-matches the previously stored value at that location and there have been no + // intervening loads from that location. + for (func_name, store_indices) in &store_indices_by_func { let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { let stats = funcs.get(func_name.as_str()).unwrap(); @@ -777,40 +785,66 @@ impl Trace { } else { (1, 0) }; + + let load_indices = load_indices_by_func + .get(func_name.as_str()) + .map(Vec::as_slice) + .unwrap_or(&[]); + // None = no store has landed here yet; Some(bits) = last stored value as u64 bits. let mut last_values = vec![None::; w * h * channels]; let mut redundant_counts = vec![0i32; w * h]; - for &idx in indices { - let pkt = &packets[idx]; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for lane in 0..n_lanes { - let Some(v) = pkt.decoded_value(lane) else { - continue; - }; - let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); - if x < 0 || y < 0 || x as usize >= w || y as usize >= h { - continue; - } - let c = if dims_per_lane >= 3 { - pkt.coordinates[2 * n_lanes + lane] - min_c - } else { - 0 - }; - if c < 0 || c as usize >= channels { - continue; - } - let val_idx = (y as usize * w + x as usize) * channels + c as usize; - let pixel_idx = y as usize * w + x as usize; - let v_bits = v.to_bits(); - if let Some(prev_bits) = last_values[val_idx] { - if prev_bits == v_bits { - redundant_counts[pixel_idx] += 1; - } - } - last_values[val_idx] = Some(v_bits); + let mut si = 0; + let mut li = 0; + + while si < store_indices.len() || li < load_indices.len() { + let next_is_store = si < store_indices.len() + && (li >= load_indices.len() || store_indices[si] < load_indices[li]); + + if next_is_store { + let global_idx = store_indices[si]; + si += 1; + + let pkt = &packets[global_idx]; + for_each_lane_pixel( + pkt, + min_x, + min_y, + w, + h, + Some((min_c, channels)), + |lane, pixel_idx, val_idx| { + let Some(v) = pkt.decoded_value(lane) else { + return; + }; + let v_bits = v.to_bits(); + if let Some(prev_bits) = last_values[val_idx] { + if prev_bits == v_bits { + redundant_counts[pixel_idx] += 1; + } + } + last_values[val_idx] = Some(v_bits); + }, + ); + } else { + // If we observe a Load, reset the last_values slot for that location to None. + let global_idx = load_indices[li]; + li += 1; + let pkt = &packets[global_idx]; + for_each_lane_pixel( + pkt, + min_x, + min_y, + w, + h, + Some((min_c, channels)), + |_lane, _pixel_idx, val_idx| { + last_values[val_idx] = None; + }, + ); } } + if let Some(stats) = funcs.get_mut(func_name.as_str()) { let (max, hist) = count_histogram(&redundant_counts); stats.max_redundant_count = max; @@ -864,52 +898,37 @@ impl Trace { let global_idx = store_indices[si]; si += 1; let pkt = &packets[global_idx]; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for lane in 0..n_lanes { - let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); - if x < 0 || y < 0 || x as usize >= w || y as usize >= h { - continue; - } - let c = if dims_per_lane >= 3 { - pkt.coordinates[2 * n_lanes + lane] - min_c - } else { - 0 - }; - if c < 0 || c as usize >= channels { - continue; - } - let val_idx = (y as usize * w + x as usize) * channels + c as usize; - last_store_at[val_idx] = global_idx; - } + for_each_lane_pixel( + pkt, + min_x, + min_y, + w, + h, + Some((min_c, channels)), + |_lane, _pixel_idx, val_idx| { + last_store_at[val_idx] = global_idx; + }, + ); } else { let global_idx = load_indices[li]; li += 1; let pkt = &packets[global_idx]; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for lane in 0..n_lanes { - let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); - if x < 0 || y < 0 || x as usize >= w || y as usize >= h { - continue; - } - let c = if dims_per_lane >= 3 { - pkt.coordinates[2 * n_lanes + lane] - min_c - } else { - 0 - }; - if c < 0 || c as usize >= channels { - continue; - } - let val_idx = (y as usize * w + x as usize) * channels + c as usize; - let pixel_idx = y as usize * w + x as usize; - if last_store_at[val_idx] != usize::MAX { - let dist = (global_idx - last_store_at[val_idx]) as i64; - if dist > max_reuse_distances[pixel_idx] { - max_reuse_distances[pixel_idx] = dist; + for_each_lane_pixel( + pkt, + min_x, + min_y, + w, + h, + Some((min_c, channels)), + |_lane, pixel_idx, val_idx| { + if last_store_at[val_idx] != usize::MAX { + let dist = (global_idx - last_store_at[val_idx]) as i64; + if dist > max_reuse_distances[pixel_idx] { + max_reuse_distances[pixel_idx] = dist; + } } - } - } + }, + ); } } @@ -945,32 +964,24 @@ impl Trace { for &global_idx in load_indices { let pkt = &packets[global_idx]; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for lane in 0..n_lanes { - let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); - if x < 0 || y < 0 || x as usize >= w || y as usize >= h { - continue; - } - let c = if dims_per_lane >= 3 { - pkt.coordinates[2 * n_lanes + lane] - min_c - } else { - 0 - }; - if c < 0 || c as usize >= channels { - continue; - } - let val_idx = (y as usize * w + x as usize) * channels + c as usize; - let pixel_idx = y as usize * w + x as usize; - if first_load_at[val_idx] == usize::MAX { - first_load_at[val_idx] = global_idx; - } else { - let dist = (global_idx - first_load_at[val_idx]) as i64; - if dist > max_reuse_distances[pixel_idx] { - max_reuse_distances[pixel_idx] = dist; + for_each_lane_pixel( + pkt, + min_x, + min_y, + w, + h, + Some((min_c, channels)), + |_lane, pixel_idx, val_idx| { + if first_load_at[val_idx] == usize::MAX { + first_load_at[val_idx] = global_idx; + } else { + let dist = (global_idx - first_load_at[val_idx]) as i64; + if dist > max_reuse_distances[pixel_idx] { + max_reuse_distances[pixel_idx] = dist; + } } - } - } + }, + ); } if let Some(stats) = funcs.get_mut(func_name.as_str()) { @@ -1137,3 +1148,47 @@ pub(crate) fn pixel_xy( }; (x, y) } + +/// Iterates over each lane of `pkt` that falls within the Func's `w x h` extents, invoking +/// `f(lane, pixel_idx, val_idx)`. `pixel_idx` is the flattened `y * w + x` location. +/// +/// When `channel` is `Some((min_c, channels))`, lanes are additionally filtered to those whose +/// channel coordinate falls within `0..channels`, and `val_idx` is the flattened +/// `pixel_idx * channels + c` location; otherwise `val_idx` is just `pixel_idx`. +fn for_each_lane_pixel( + pkt: &TracePacket, + min_x: i32, + min_y: i32, + w: usize, + h: usize, + channel: Option<(i32, usize)>, + mut f: impl FnMut(usize, usize, usize), +) { + let n_lanes = pkt.type_.lanes.max(1) as usize; + let dims_per_lane = pkt.coordinates.len() / n_lanes; + for lane in 0..n_lanes { + let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); + if x < 0 || y < 0 || x as usize >= w || y as usize >= h { + continue; + } + + let pixel_idx = y as usize * w + x as usize; + let val_idx = if let Some((min_c, channels)) = channel { + let c = if dims_per_lane >= 3 { + pkt.coordinates[2 * n_lanes + lane] - min_c + } else { + 0 + }; + + if c < 0 || c as usize >= channels { + continue; + } + + pixel_idx * channels + c as usize + } else { + pixel_idx + }; + + f(lane, pixel_idx, val_idx); + } +} From 1c2e216e24a44625ce098c6e2f0a934bd1b389d0 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Mon, 13 Jul 2026 10:13:09 -0700 Subject: [PATCH 22/67] Add support for normalization modes and real-time histogram rendering. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/src-tauri/Cargo.lock | 658 ------------------ apps/halidoscope/src-tauri/Cargo.toml | 1 - apps/halidoscope/src-tauri/src/cli.rs | 102 +-- apps/halidoscope/src-tauri/src/commands.rs | 62 +- apps/halidoscope/src-tauri/src/render.rs | 190 ++++- apps/halidoscope/src-tauri/src/trace.rs | 74 +- apps/halidoscope/src/App.tsx | 22 +- .../src/components/canvas/FuncNode.tsx | 70 +- .../controls/VisualizationPanel.tsx | 94 ++- .../controls/histogram/Histogram.tsx | 10 +- .../histogram/HistogramParameters.tsx | 175 +++++ .../controls/histogram/HistogramSelect.tsx | 126 ---- .../components/controls/render/RenderMode.tsx | 10 +- .../src/components/icons/ArrowDownIcon.tsx | 20 + apps/halidoscope/src/hooks/trace.ts | 6 - apps/halidoscope/src/state/histogram.ts | 8 +- apps/halidoscope/src/state/render.ts | 6 +- apps/halidoscope/src/types/index.ts | 7 - apps/halidoscope/src/utils/api.ts | 95 ++- 19 files changed, 607 insertions(+), 1129 deletions(-) create mode 100644 apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx delete mode 100644 apps/halidoscope/src/components/controls/histogram/HistogramSelect.tsx create mode 100644 apps/halidoscope/src/components/icons/ArrowDownIcon.tsx diff --git a/apps/halidoscope/src-tauri/Cargo.lock b/apps/halidoscope/src-tauri/Cargo.lock index 6bfd545bc73f..7d78f17b5c77 100644 --- a/apps/halidoscope/src-tauri/Cargo.lock +++ b/apps/halidoscope/src-tauri/Cargo.lock @@ -17,24 +17,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -115,38 +97,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "arrayvec" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "async-broadcast" version = "0.7.2" @@ -313,49 +263,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec", - "log", - "num-rational", - "num-traits", - "pastey", - "rayon", - "thiserror 2.0.18", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" -dependencies = [ - "arrayvec", -] - [[package]] name = "base64" version = "0.21.7" @@ -383,12 +290,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - [[package]] name = "bitflags" version = "1.3.2" @@ -404,15 +305,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "bitstream-io" -version = "4.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" -dependencies = [ - "no_std_io2", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -474,12 +366,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "built" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" - [[package]] name = "bumpalo" version = "3.20.3" @@ -498,12 +384,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "bytes" version = "1.11.1" @@ -587,8 +467,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -664,12 +542,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.5" @@ -778,37 +650,12 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.7" @@ -1077,12 +924,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - [[package]] name = "embed-resource" version = "3.0.9" @@ -1130,26 +971,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1198,33 +1019,12 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "exr" -version = "1.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -[[package]] -name = "fax" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" - [[package]] name = "fdeflate" version = "0.3.7" @@ -1543,16 +1343,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "gif" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "gio" version = "0.18.4" @@ -1701,23 +1491,11 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - [[package]] name = "halidoscope" version = "0.1.0" dependencies = [ "colorous", - "image", "serde", "serde_json", "tauri", @@ -2012,46 +1790,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.1", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imgref" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" - [[package]] name = "indexmap" version = "1.9.3" @@ -2084,17 +1822,6 @@ dependencies = [ "cfb", ] -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -2126,15 +1853,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" @@ -2208,16 +1926,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.99" @@ -2269,12 +1977,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "lebe" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" - [[package]] name = "libappindicator" version = "0.9.0" @@ -2314,16 +2016,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "libfuzzer-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" -dependencies = [ - "arbitrary", - "cc", -] - [[package]] name = "libloading" version = "0.7.4" @@ -2370,15 +2062,6 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - [[package]] name = "markup5ever" version = "0.38.0" @@ -2390,16 +2073,6 @@ dependencies = [ "web_atoms", ] -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - [[package]] name = "memchr" version = "2.8.1" @@ -2442,16 +2115,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - [[package]] name = "muda" version = "0.19.2" @@ -2503,77 +2166,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "no_std_io2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" -dependencies = [ - "memchr", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -2894,18 +2492,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - [[package]] name = "pathdiff" version = "0.2.3" @@ -3062,15 +2648,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "precomputed-hash" version = "0.1.1" @@ -3149,46 +2726,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "profiling" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pxfm" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quick-xml" version = "0.39.4" @@ -3219,111 +2756,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand", - "rand_chacha", - "simd_helpers", - "thiserror 2.0.18", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", -] - [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -3427,12 +2865,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "rgb" -version = "0.8.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" - [[package]] name = "rustc-hash" version = "2.1.2" @@ -3753,15 +3185,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - [[package]] name = "siphasher" version = "1.0.3" @@ -4325,20 +3748,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - [[package]] name = "time" version = "0.3.47" @@ -4783,17 +4192,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - [[package]] name = "version-compare" version = "0.2.1" @@ -5073,12 +4471,6 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - [[package]] name = "winapi" version = "0.3.9" @@ -5629,12 +5021,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - [[package]] name = "yoke" version = "0.8.2" @@ -5719,26 +5105,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "zerofrom" version = "0.1.8" @@ -5799,30 +5165,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] - [[package]] name = "zvariant" version = "5.12.0" diff --git a/apps/halidoscope/src-tauri/Cargo.toml b/apps/halidoscope/src-tauri/Cargo.toml index 110ee19e7258..d436f7a2af86 100644 --- a/apps/halidoscope/src-tauri/Cargo.toml +++ b/apps/halidoscope/src-tauri/Cargo.toml @@ -23,7 +23,6 @@ tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" colorous = "1.0.16" -image = "0.25.10" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-cli = "2.0.0" diff --git a/apps/halidoscope/src-tauri/src/cli.rs b/apps/halidoscope/src-tauri/src/cli.rs index c92f40d26feb..6ee6ff77773c 100644 --- a/apps/halidoscope/src-tauri/src/cli.rs +++ b/apps/halidoscope/src-tauri/src/cli.rs @@ -1,15 +1,10 @@ use std::ffi::OsStr; use std::path::Path; -use image; use tauri_plugin_cli::SubcommandMatches; use crate::graph::to_dot; -use crate::render::{ - GrayscaleState, LoadFrequencyState, RedundantState, Renderer, ReuseDistanceState, RgbState, - StoreFrequencyState, -}; -use crate::trace::{func_extents, Trace}; +use crate::trace::Trace; pub fn halidoscope_cli(subcommand: Box) { match subcommand.name.as_str() { @@ -35,11 +30,11 @@ pub fn halidoscope_cli(subcommand: Box) { .and_then(|a| a.value.as_str()) .and_then(|s| s.parse::().ok()) .unwrap_or(0); - let mode = args + let _mode = args .get("mode") .and_then(|a| a.value.as_str()) .unwrap_or("grayscale"); - let destination = args + let _destination = args .get("destination") .and_then(|a| a.value.as_str()) .unwrap_or_else(|| { @@ -54,7 +49,7 @@ pub fn halidoscope_cli(subcommand: Box) { }); // Find the target function. - let target_func = tr.funcs.get(func).unwrap_or_else(|| { + let _target_func = tr.funcs.get(func).unwrap_or_else(|| { eprintln!( "Func '{}' not found in trace. Available Funcs: {:?}", func, @@ -73,61 +68,8 @@ pub fn halidoscope_cli(subcommand: Box) { std::process::exit(1); } - let store_indices = tr.func_store_indices(func).unwrap_or(&[]); - let load_indices = tr.func_load_indices(func).unwrap_or(&[]); - - let buffer = match mode { - "grayscale" => { - write_buffer::(&tr, func, store_indices, packet_index) - } - "rgb" => write_buffer::(&tr, func, store_indices, packet_index), - "store-frequency" => { - write_buffer::(&tr, func, store_indices, packet_index) - } - "load-frequency" => { - write_buffer::(&tr, func, load_indices, packet_index) - } - "redundant-stores" => { - write_buffer::(&tr, func, store_indices, packet_index) - } - "reuse-distance" => write_reuse_distance_buffer( - &tr, - func, - store_indices, - load_indices, - packet_index, - ), - _ => { - eprintln!("Unknown rendering mode: {}", mode); - std::process::exit(1); - } - }; - - if let Some((width, height, _, _)) = func_extents(target_func) { - match image::save_buffer( - &destination, - &buffer, - width as u32, - height as u32, - image::ColorType::Rgba8, - ) { - Ok(_) => { - println!("Snapshot written to {}", destination); - std::process::exit(0); - } - Err(e) => { - eprintln!("Error saving snapshot: {}", e); - std::process::exit(1); - } - } - } - - // If we reach here, it means we couldn't determine the dimensions of the Func. - eprintln!( - "Could not determine dimensions for Func '{}'. Ensure it has valid geometry.", - func - ); - std::process::exit(1); + // TODO: Convert this command to write out numeric values to CSV / JSON. + std::process::exit(0); } "dot" => { let args = &subcommand.matches.args; @@ -182,35 +124,3 @@ pub fn halidoscope_cli(subcommand: Box) { } } } - -fn write_buffer( - trace: &Trace, - func: &str, - indices: &[usize], - packet_index: u32, -) -> Vec { - if let Some(mut state) = R::register(trace, func) { - let k = indices.partition_point(|&p| p <= packet_index as usize); - state.seek(trace, indices, k); - state.to_rgba() - } else { - Vec::new() - } -} - -fn write_reuse_distance_buffer( - trace: &Trace, - func: &str, - store_indices: &[usize], - load_indices: &[usize], - packet_index: u32, -) -> Vec { - if let Some(mut state) = ReuseDistanceState::new(trace, func) { - let store_k = store_indices.partition_point(|&p| p <= packet_index as usize); - let load_k = load_indices.partition_point(|&p| p <= packet_index as usize); - state.seek(trace, store_indices, load_indices, store_k, load_k); - state.to_rgba() - } else { - Vec::new() - } -} diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index d21725f6ebcf..90460fe7365d 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -10,8 +10,8 @@ use tauri::ipc::Response; use tauri::State; use crate::render::{ - GrayscaleState, LoadFrequencyState, RedundantState, Renderer, ReuseDistanceState, RgbState, - StoreFrequencyState, + GrayscaleState, LoadFrequencyState, NormalizationMode, RedundantState, Renderer, + ReuseDistanceState, RgbState, StoreFrequencyState, }; use crate::trace::Trace; @@ -44,10 +44,6 @@ pub struct FuncMeta { pub max_load_count: i32, pub max_redundant_count: i32, pub max_reuse_distance: i64, - pub store_count_histogram: Vec, - pub load_count_histogram: Vec, - pub redundant_count_histogram: Vec, - pub reuse_distance_histogram: Vec, pub buffer_liveness: IndexRange, pub produce_ranges: Vec, pub consume_ranges: Vec, @@ -60,9 +56,6 @@ pub struct TraceMeta { pub funcs: Vec, pub total_packets: u32, pub dag_edges: BTreeMap>, - pub global_max_store_count: i32, - pub global_max_load_count: i32, - pub global_max_redundant_count: i32, pub global_max_reuse_distance: i64, } @@ -71,9 +64,6 @@ impl TraceMeta { /// are still listed (with zero dimensions) so the UI can surface them; the renderer simply /// produces nothing for them. pub fn from_trace(trace: &Trace) -> Self { - let mut global_max_store_count = 0; - let mut global_max_load_count = 0; - let mut global_max_redundant_count = 0; let mut global_max_reuse_distance = 0i64; let funcs = trace @@ -88,10 +78,6 @@ impl TraceMeta { let stores = trace.func_store_indices(name); let num_stores = stores.map(<[usize]>::len).unwrap_or(0) as u32; - global_max_store_count = stats.max_store_count.max(global_max_store_count); - global_max_load_count = stats.max_load_count.max(global_max_load_count); - global_max_redundant_count = - stats.max_redundant_count.max(global_max_redundant_count); global_max_reuse_distance = stats.max_reuse_distance.max(global_max_reuse_distance); FuncMeta { @@ -108,10 +94,6 @@ impl TraceMeta { max_load_count: stats.max_load_count, max_redundant_count: stats.max_redundant_count, max_reuse_distance: stats.max_reuse_distance, - store_count_histogram: stats.store_count_histogram.clone(), - load_count_histogram: stats.load_count_histogram.clone(), - redundant_count_histogram: stats.redundant_count_histogram.clone(), - reuse_distance_histogram: stats.reuse_distance_histogram.clone(), buffer_liveness: IndexRange::from_tuple( trace .func_buffer_liveness_range(name) @@ -151,9 +133,6 @@ impl TraceMeta { funcs, total_packets: trace.packets.len() as u32, dag_edges, - global_max_store_count, - global_max_load_count, - global_max_redundant_count, global_max_reuse_distance, } } @@ -180,6 +159,17 @@ pub struct AppState { inner: Mutex>, } +/// Appends `histogram`'s bins as little-endian `u32`s directly after `pixels`, so a single +/// `Response` carries both. The frontend already knows the pixel-buffer length ahead of time +/// (`width * height * 4`), so no length prefix is needed to split the two back apart. +fn pack_pixels_and_histogram(mut pixels: Vec, histogram: Vec) -> Vec { + pixels.reserve(histogram.len() * 4); + for bin in histogram { + pixels.extend_from_slice(&bin.to_le_bytes()); + } + pixels +} + // ── Commands ────────────────────────────────────────────────────────────────── /// Parses a `.hltrace` file and returns the metadata the frontend needs to set up canvases and @@ -208,6 +198,7 @@ pub fn open_trace(path: String, state: State) -> Result, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -229,7 +220,7 @@ pub fn render_grayscale( let k = store_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, store_indices, k); - Ok(Response::new(renderer.to_rgba())) + Ok(Response::new(renderer.to_rgba(normalization_mode))) } /// Renders `func` as an RGB image at `global_index` and returns raw RGBA8 bytes. Planes 0/1/2 @@ -238,6 +229,7 @@ pub fn render_grayscale( pub fn render_rgb( func: String, global_index: u32, + normalization_mode: NormalizationMode, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -259,7 +251,7 @@ pub fn render_rgb( let k = store_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, store_indices, k); - Ok(Response::new(renderer.to_rgba())) + Ok(Response::new(renderer.to_rgba(normalization_mode))) } /// Renders a heatmap of store counts for `func` up to `global_index` and returns raw RGBA8 bytes. @@ -269,6 +261,7 @@ pub fn render_rgb( pub fn render_store_frequency( func: String, global_index: u32, + normalization_mode: NormalizationMode, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -292,7 +285,9 @@ pub fn render_store_frequency( let k = store_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, store_indices, k); - Ok(Response::new(renderer.to_rgba())) + let pixels = renderer.to_rgba(normalization_mode); + let histogram = renderer.to_histogram(normalization_mode); + Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) } /// Renders a heatmap of load counts for `func` up to `global_index` and returns raw RGBA8 bytes. @@ -302,6 +297,7 @@ pub fn render_store_frequency( pub fn render_load_frequency( func: String, global_index: u32, + normalization_mode: NormalizationMode, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -325,7 +321,9 @@ pub fn render_load_frequency( let k = load_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, load_indices, k); - Ok(Response::new(renderer.to_rgba())) + let pixels = renderer.to_rgba(normalization_mode); + let histogram = renderer.to_histogram(normalization_mode); + Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) } /// Renders a heatmap of redundant store counts for `func` up to `global_index` and returns raw @@ -336,6 +334,7 @@ pub fn render_load_frequency( pub fn render_redundant_stores( func: String, global_index: u32, + normalization_mode: NormalizationMode, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -357,7 +356,9 @@ pub fn render_redundant_stores( let k = store_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, store_indices, k); - Ok(Response::new(renderer.to_rgba())) + let pixels = renderer.to_rgba(normalization_mode); + let histogram = renderer.to_histogram(normalization_mode); + Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) } /// Renders a heatmap of maximum store-to-load reuse distances for `func` up to `global_index` @@ -369,6 +370,7 @@ pub fn render_redundant_stores( pub fn render_reuse_distance( func: String, global_index: u32, + normalization_mode: NormalizationMode, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -394,5 +396,7 @@ pub fn render_reuse_distance( let load_k = load_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, store_indices, load_indices, store_k, load_k); - Ok(Response::new(renderer.to_rgba())) + let pixels = renderer.to_rgba(normalization_mode); + let histogram = renderer.to_histogram(normalization_mode); + Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) } diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index 27266c9041c5..e3c3172bb86f 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -5,14 +5,23 @@ //! passed into `seek` so states can live in Tauri-managed storage alongside the parsed trace. use ::colorous; +use serde::Deserialize; use crate::trace::{pixel_xy, FuncGeometry, Trace, TracePacket}; -// A trait that all rendering states implement. +#[derive(Deserialize, Clone, Copy)] +pub enum NormalizationMode { + #[serde(rename = "Across Funcs")] + AcrossFuncs, + #[serde(rename = "Per Func")] + PerFunc, +} + +// A trait that all 2D rendering states implement. pub trait Renderer: Sized { fn register(trace: &Trace, func: &str) -> Option; fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize); - fn to_rgba(&self) -> Vec; + fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec; } // ── Grayscale rendering ─────────────────────────────────────────────────────── @@ -102,7 +111,7 @@ impl Renderer for GrayscaleState { } /// Produces a `width * height * 4` RGBA8 buffer. Channel 0 is replicated across R/G/B. - fn to_rgba(&self) -> Vec { + fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, @@ -221,7 +230,7 @@ impl Renderer for RgbState { /// Produces a `width * height * 4` RGBA8 buffer. Planes 0/1/2 map to R/G/B; /// missing planes are 0. Alpha is always opaque. - fn to_rgba(&self) -> Vec { + fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, @@ -257,6 +266,7 @@ impl Renderer for RgbState { pub struct StoreFrequencyState { geom: FuncGeometry, counts: Vec, + local_max_store_count: i32, global_max_store_count: i32, applied_k: usize, } @@ -265,15 +275,19 @@ impl StoreFrequencyState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let counts = vec![0i32; geom.width * geom.height]; + + let local_max_store_count = trace.funcs.get(func).map_or(0, |s| s.max_store_count); let global_max_store_count = trace .funcs .values() .map(|s| s.max_store_count) .max() .unwrap_or(0); + Some(Self { geom, counts, + local_max_store_count, global_max_store_count, applied_k: 0, }) @@ -318,18 +332,27 @@ impl Renderer for StoreFrequencyState { /// Produces a `width × height × 4` RGBA8 buffer with the Inferno colormap applied. Counts /// are normalized against the global full-trace maximum so intensities are comparable across /// all Funcs. - fn to_rgba(&self) -> Vec { + fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { let c = gradient.eval_continuous(i as f64 / 255.0); [c.r, c.g, c.b] }); - let scale = if self.global_max_store_count > 0 { - 255.0 / self.global_max_store_count as f64 - } else { - 0.0 + + let scale = match (normalization_mode, self.global_max_store_count) { + (NormalizationMode::AcrossFuncs, 0) => 0.0, + (NormalizationMode::AcrossFuncs, max) => 255.0 / max as f64, + (NormalizationMode::PerFunc, _) => { + let local_max = *&self.local_max_store_count; + if local_max > 0 { + 255.0 / local_max as f64 + } else { + 0.0 + } + } }; + let mut out = vec![0u8; width * height * 4]; for (chunk, &count) in out.chunks_exact_mut(4).zip(self.counts.iter()) { let ti = (count as f64 * scale) as usize; @@ -343,6 +366,24 @@ impl Renderer for StoreFrequencyState { } } +impl StoreFrequencyState { + /// Produces a frequency histogram of live per-pixel store counts at the current seek + /// position. `hist[k]` is the number of pixel locations currently stored exactly `k` times, + /// for `k` in `0..=chosen_max`, where `chosen_max` is this Func's own max (`PerFunc`) or the + /// trace-wide max (`AcrossFuncs`) — matching whichever max drives `to_rgba`'s color scale. + pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + let chosen_max = match normalization_mode { + NormalizationMode::AcrossFuncs => self.global_max_store_count, + NormalizationMode::PerFunc => self.local_max_store_count, + }; + let mut hist = vec![0u32; chosen_max as usize + 1]; + for &c in &self.counts { + hist[c.clamp(0, chosen_max) as usize] += 1; + } + hist + } +} + // ── Load frequency rendering ────────────────────────────────────────────────── /// Mirrors `StoreFrequencyState` but tracks load events instead of store events. The global max @@ -350,6 +391,7 @@ impl Renderer for StoreFrequencyState { pub struct LoadFrequencyState { geom: FuncGeometry, counts: Vec, + local_max_load_count: i32, global_max_load_count: i32, applied_k: usize, } @@ -358,15 +400,19 @@ impl LoadFrequencyState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let counts = vec![0i32; geom.width * geom.height]; + + let local_max_load_count = trace.funcs.get(func).map_or(0, |s| s.max_load_count); let global_max_load_count = trace .funcs .values() .map(|s| s.max_load_count) .max() .unwrap_or(0); + Some(Self { geom, counts, + local_max_load_count, global_max_load_count, applied_k: 0, }) @@ -411,18 +457,26 @@ impl Renderer for LoadFrequencyState { /// Produces a `width × height × 4` RGBA8 buffer with the Inferno colormap applied. Counts /// are normalized against the global full-trace maximum so intensities are comparable across /// all Funcs. - fn to_rgba(&self) -> Vec { + fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { let c = gradient.eval_continuous(i as f64 / 255.0); [c.r, c.g, c.b] }); - let scale = if self.global_max_load_count > 0 { - 255.0 / self.global_max_load_count as f64 - } else { - 0.0 + + let scale = match (normalization_mode, self.global_max_load_count) { + (NormalizationMode::AcrossFuncs, 0) => 0.0, + (NormalizationMode::AcrossFuncs, max) => 255.0 / max as f64, + (NormalizationMode::PerFunc, _) => { + if self.local_max_load_count > 0 { + 255.0 / self.local_max_load_count as f64 + } else { + 0.0 + } + } }; + let mut out = vec![0u8; width * height * 4]; for (chunk, &count) in out.chunks_exact_mut(4).zip(self.counts.iter()) { let ti = (count as f64 * scale) as usize; @@ -436,6 +490,24 @@ impl Renderer for LoadFrequencyState { } } +impl LoadFrequencyState { + /// Produces a frequency histogram of live per-pixel load counts at the current seek + /// position. `hist[k]` is the number of pixel locations currently loaded exactly `k` times, + /// for `k` in `0..=chosen_max`, where `chosen_max` is this Func's own max (`PerFunc`) or the + /// trace-wide max (`AcrossFuncs`) — matching whichever max drives `to_rgba`'s color scale. + pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + let chosen_max = match normalization_mode { + NormalizationMode::AcrossFuncs => self.global_max_load_count, + NormalizationMode::PerFunc => self.local_max_load_count, + }; + let mut hist = vec![0u32; chosen_max as usize + 1]; + for &c in &self.counts { + hist[c.clamp(0, chosen_max) as usize] += 1; + } + hist + } +} + // ── Redundant computation rendering ────────────────────────────────────────── /// Accumulated per-pixel redundant-store counts for one Func. A store to pixel (x, y, c) is @@ -450,6 +522,7 @@ pub struct RedundantState { last_values: Vec>, /// Redundant-store count per spatial pixel, indexed by `y * width + x`. redundant_counts: Vec, + local_max_redundant_count: i32, global_max_redundant_count: i32, applied_k: usize, } @@ -459,16 +532,21 @@ impl RedundantState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let n_pixels = geom.width * geom.height; + + let local_max_redundant_count = + trace.funcs.get(func).map_or(0, |s| s.max_redundant_count); let global_max_redundant_count = trace .funcs .values() .map(|s| s.max_redundant_count) .max() .unwrap_or(0); + Some(Self { geom, last_values: vec![None; n_pixels * geom.channels], redundant_counts: vec![0i32; n_pixels], + local_max_redundant_count, global_max_redundant_count, applied_k: 0, }) @@ -543,7 +621,7 @@ impl Renderer for RedundantState { /// Produces a `width × height × 4` RGBA8 buffer. Pixels with zero redundant stores are black; /// pixels with one or more are mapped through the Inferno colormap, normalized against the /// global full-trace maximum so intensities are comparable across all Funcs. - fn to_rgba(&self) -> Vec { + fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; @@ -551,10 +629,17 @@ impl Renderer for RedundantState { let c = gradient.eval_continuous(i as f64 / 255.0); [c.r, c.g, c.b] }); - let scale = if self.global_max_redundant_count > 0 { - 255.0 / self.global_max_redundant_count as f64 - } else { - 0.0 + + let scale = match (normalization_mode, self.global_max_redundant_count) { + (NormalizationMode::AcrossFuncs, 0) => 0.0, + (NormalizationMode::AcrossFuncs, max) => 255.0 / max as f64, + (NormalizationMode::PerFunc, _) => { + if self.local_max_redundant_count > 0 { + 255.0 / self.local_max_redundant_count as f64 + } else { + 0.0 + } + } }; let mut out = vec![0u8; width * height * 4]; @@ -572,6 +657,25 @@ impl Renderer for RedundantState { } } +impl RedundantState { + /// Produces a frequency histogram of live per-pixel redundant-store counts at the current + /// seek position. `hist[k]` is the number of pixel locations with exactly `k` redundant + /// stores so far, for `k` in `0..=chosen_max`, where `chosen_max` is this Func's own max + /// (`PerFunc`) or the trace-wide max (`AcrossFuncs`) — matching whichever max drives + /// `to_rgba`'s color scale. + pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + let chosen_max = match normalization_mode { + NormalizationMode::AcrossFuncs => self.global_max_redundant_count, + NormalizationMode::PerFunc => self.local_max_redundant_count, + }; + let mut hist = vec![0u32; chosen_max as usize + 1]; + for &c in &self.redundant_counts { + hist[c.clamp(0, chosen_max) as usize] += 1; + } + hist + } +} + // ── Reuse distance rendering ────────────────────────────────────────────────── /// Per-pixel maximum reuse distance for one Func, seekable along the global timeline. @@ -595,6 +699,9 @@ pub struct ReuseDistanceState { anchor_at: Vec, /// Maximum observed reuse distance per spatial pixel, indexed by `y * width + x`. max_reuse_distance: Vec, + /// This Func's own maximum reuse distance, used to normalize the color scale against just + /// this Func's range. + local_max_reuse_distance: i64, /// Trace-wide maximum reuse distance, used to normalize the color scale consistently across /// all Funcs regardless of which one is being viewed. global_max_reuse_distance: i64, @@ -613,17 +720,21 @@ impl ReuseDistanceState { let is_input = trace .func_store_indices(func) .map_or(true, |s| s.is_empty()); + + let local_max_reuse_distance = trace.funcs.get(func).map_or(0, |s| s.max_reuse_distance); let global_max_reuse_distance = trace .funcs .values() .map(|s| s.max_reuse_distance) .max() .unwrap_or(0); + Some(Self { geom, is_input, anchor_at: vec![usize::MAX; n_cells], max_reuse_distance: vec![0i64; geom.width * geom.height], + local_max_reuse_distance, global_max_reuse_distance, applied_store_k: 0, applied_load_k: 0, @@ -761,7 +872,7 @@ impl ReuseDistanceState { /// Produces a `width × height × 4` RGBA8 buffer. Pixels with no observed store→load pair are /// black; positive distances map through the Inferno colormap normalized against the per-Func /// full-trace maximum so the scale is stable while scrubbing. - pub fn to_rgba(&self) -> Vec { + pub fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; @@ -769,10 +880,17 @@ impl ReuseDistanceState { let c = gradient.eval_continuous(i as f64 / 255.0); [c.r, c.g, c.b] }); - let scale = if self.global_max_reuse_distance > 0 { - 255.0 / self.global_max_reuse_distance as f64 - } else { - 0.0 + + let scale = match (normalization_mode, self.global_max_reuse_distance) { + (NormalizationMode::AcrossFuncs, 0) => 0.0, + (NormalizationMode::AcrossFuncs, max) => 255.0 / max as f64, + (NormalizationMode::PerFunc, _) => { + if self.local_max_reuse_distance > 0 { + 255.0 / self.local_max_reuse_distance as f64 + } else { + 0.0 + } + } }; let mut out = vec![0u8; width * height * 4]; @@ -788,4 +906,28 @@ impl ReuseDistanceState { } out } + + /// Produces a fixed 64-bucket histogram of live per-pixel maximum reuse distances at the + /// current seek position. Bucket `k` covers distances in `[k/63 * chosen_max, (k+1)/63 * + /// chosen_max)`, with bucket 63 inclusive of `chosen_max`. Pixels with no observed + /// store→load pair (distance 0) are excluded. `chosen_max` is this Func's own max + /// (`PerFunc`) or the trace-wide max (`AcrossFuncs`) — matching whichever max drives + /// `to_rgba`'s color scale. + pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + let chosen_max = match normalization_mode { + NormalizationMode::AcrossFuncs => self.global_max_reuse_distance, + NormalizationMode::PerFunc => self.local_max_reuse_distance, + }; + + let mut hist = vec![0u32; 64]; + if chosen_max > 0 { + for &dist in &self.max_reuse_distance { + if dist > 0 { + let bucket = ((dist as f64 / chosen_max as f64) * 63.0) as usize; + hist[bucket.min(63)] += 1; + } + } + } + hist + } } diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 92a0fa7a1cbe..9ab8669eb358 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -181,23 +181,6 @@ pub struct FuncStats { /// Measured as thedifference in global packet indices between a store and the next load from /// the same coordination. 0 when no store→load pair was observed. pub max_reuse_distance: i64, - /// Frequency distribution of per-coordinate store counts. `hist[k]` is the number of pixel - /// locations stored exactly `k` times, for `k` in `0..=max_store_count`. The `0` bin is - /// included. Empty when the Func has no usable extent. - pub store_count_histogram: Vec, - /// Frequency distribution of per-coordinate load counts. `hist[k]` is the number of pixel - /// locations loaded exactly `k` times, for `k` in `0..=max_load_count`. The `0` bin is - /// included. Empty when the Func has no usable extent. - pub load_count_histogram: Vec, - /// Frequency distribution of per-coordinate redundant store counts. `hist[k]` is the number of - /// pixel locations with exactly `k` redundant stores, for `k` in `0..=max_redundant_count`. - /// The `0` bin is included. Empty when the Func has no usable extent. - pub redundant_count_histogram: Vec, - /// Fixed-width 64-bucket histogram of per-coordinate maximum reuse distances. Bucket `k` covers - /// distances in `[k/63 * max, (k+1)/63 * max)`, with bucket 63 inclusive of `max`. Pixels - /// with no observed store→load pair (distance 0) are excluded. Empty when - /// `max_reuse_distance == 0`. - pub reuse_distance_histogram: Vec, } impl Default for FuncStats { @@ -212,10 +195,6 @@ impl Default for FuncStats { max_load_count: 0, max_redundant_count: 0, max_reuse_distance: 0, - store_count_histogram: vec![], - load_count_histogram: vec![], - redundant_count_histogram: vec![], - reuse_distance_histogram: vec![], } } } @@ -736,9 +715,7 @@ impl Trace { ); } if let Some(stats) = funcs.get_mut(func_name.as_str()) { - let (max, hist) = count_histogram(&counts); - stats.max_store_count = max; - stats.store_count_histogram = hist; + stats.max_store_count = counts.iter().copied().max().unwrap_or(0); } } } @@ -762,9 +739,7 @@ impl Trace { ); } if let Some(stats) = funcs.get_mut(func_name.as_str()) { - let (max, hist) = count_histogram(&counts); - stats.max_load_count = max; - stats.load_count_histogram = hist; + stats.max_load_count = counts.iter().copied().max().unwrap_or(0); } } } @@ -846,9 +821,7 @@ impl Trace { } if let Some(stats) = funcs.get_mut(func_name.as_str()) { - let (max, hist) = count_histogram(&redundant_counts); - stats.max_redundant_count = max; - stats.redundant_count_histogram = hist; + stats.max_redundant_count = redundant_counts.iter().copied().max().unwrap_or(0); } } } @@ -864,9 +837,6 @@ impl Trace { // and is "free". Subsequent loads to the same pixel measure distance from that first // load. Black = only one load ever (no reuse). // - // Per-pixel distance vecs are collected here; histogram building is deferred until the - // global max is known so all Funcs share the same bucket scale. - let mut reuse_distances_by_func: BTreeMap> = BTreeMap::new(); for (func_name, store_indices) in &store_indices_by_func { let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { @@ -936,7 +906,6 @@ impl Trace { stats.max_reuse_distance = max_reuse_distances.iter().copied().max().unwrap_or(0); } - reuse_distances_by_func.insert(func_name.clone(), max_reuse_distances); } } @@ -988,30 +957,6 @@ impl Trace { stats.max_reuse_distance = max_reuse_distances.iter().copied().max().unwrap_or(0); } - reuse_distances_by_func.insert(func_name.clone(), max_reuse_distances); - } - } - - // Build globally-normalized 64-bucket histograms: bucket boundaries are identical across - // all Funcs so the x-axis is directly comparable. - let global_max_reuse_distance: i64 = funcs - .values() - .map(|s| s.max_reuse_distance) - .max() - .unwrap_or(0); - if global_max_reuse_distance > 0 { - for (func_name, distances) in &reuse_distances_by_func { - let mut hist = vec![0u32; 64]; - for &dist in distances { - if dist > 0 { - let bucket = - (dist as f64 / global_max_reuse_distance as f64 * 63.0) as usize; - hist[bucket] += 1; - } - } - if let Some(stats) = funcs.get_mut(func_name.as_str()) { - stats.reuse_distance_histogram = hist; - } } } @@ -1092,19 +1037,6 @@ impl Trace { // ── Shared geometry helpers ─────────────────────────────────────────────────── -/// Builds a frequency histogram from per-pixel `counts`: `hist[k]` is the number of pixel -/// locations whose count is exactly `k`, for `k` in `0..=max`. The `0` bin is included so the -/// frontend can surface untouched locations. Counts are always non-negative. Returns -/// `(max_count, hist)`. -fn count_histogram(counts: &[i32]) -> (i32, Vec) { - let max = counts.iter().copied().max().unwrap_or(0); - let mut hist = vec![0u32; max as usize + 1]; - for &c in counts { - hist[c as usize] += 1; - } - (max, hist) -} - /// Returns `(width, height, min_x, min_y)` for a Func, or `None` if the stats /// have no coordinate information or produce a zero-area extent. pub fn func_extents(stats: &FuncStats) -> Option<(usize, usize, i32, i32)> { diff --git a/apps/halidoscope/src/App.tsx b/apps/halidoscope/src/App.tsx index 646395252939..ead0b8bdc1d4 100644 --- a/apps/halidoscope/src/App.tsx +++ b/apps/halidoscope/src/App.tsx @@ -15,11 +15,6 @@ function App() { const [funcs, setFuncs] = React.useState>({}); const [dagEdges, setDagEdges] = React.useState>({}); const [packetCount, setPacketCount] = React.useState(0); - const [globalMaxStoreCount, setGlobalMaxStoreCount] = - React.useState(0); - const [globalMaxLoadCount, setGlobalMaxLoadCount] = React.useState(0); - const [globalMaxRedundantCount, setGlobalMaxRedundantCount] = - React.useState(0); const [globalMaxReuseDistance, setGlobalMaxReuseDistance] = React.useState(0); @@ -39,15 +34,8 @@ function App() { : `${await invoke("get_cwd")}/${tracePath}`; try { - const { - funcs, - total_packets, - dag_edges, - global_max_store_count, - global_max_load_count, - global_max_redundant_count, - global_max_reuse_distance, - } = await openTrace(resolved); + const { funcs, total_packets, dag_edges, global_max_reuse_distance } = + await openTrace(resolved); const byName: Record = {}; for (const func of funcs) { @@ -57,9 +45,6 @@ function App() { setFuncs(byName); setDagEdges(dag_edges); setPacketCount(total_packets); - setGlobalMaxStoreCount(global_max_store_count); - setGlobalMaxLoadCount(global_max_load_count); - setGlobalMaxRedundantCount(global_max_redundant_count); setGlobalMaxReuseDistance(global_max_reuse_distance); setActiveFunc(funcs[0]?.name ?? ""); } catch (err) { @@ -76,9 +61,6 @@ function App() { funcs, dagEdges, packetCount, - globalMaxStoreCount, - globalMaxLoadCount, - globalMaxRedundantCount, globalMaxReuseDistance, }} > diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 53fcfb05bf4d..b6d6009e25be 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -11,14 +11,16 @@ import { useViewport, } from "@xyflow/react"; import { clsx } from "clsx"; -import { useAtomValue } from "jotai"; +import { useAtomValue, useSetAtom } from "jotai"; import * as React from "react"; import HandleCircle from "@/components/canvas/HandleCircle"; import { useTraceContext } from "@/hooks/trace"; +import { funcAtom } from "@/state/func"; +import { histogramAtom } from "@/state/histogram"; import { livenessAtom } from "@/state/liveness"; import { packetAtom } from "@/state/packet"; -import { renderModeAtom } from "@/state/render"; +import { renderAtom } from "@/state/render"; import type { FuncMeta } from "@/types"; import { renderGrayscale, @@ -27,6 +29,7 @@ import { renderLoadFrequency, renderRedundantStores, renderReuseDistance, + type RenderResult, } from "@/utils/api"; import { isFuncBufferLive, isEdgeLive } from "@/utils/liveness"; @@ -37,7 +40,9 @@ function FuncNode({ data }: NodeProps>) { const { funcs } = useTraceContext(); const liveness = useAtomValue(livenessAtom); const packetIndex = useAtomValue(packetAtom); - const renderMode = useAtomValue(renderModeAtom); + const render = useAtomValue(renderAtom); + const activeFunc = useAtomValue(funcAtom); + const setHistogramData = useSetAtom(histogramAtom); const nodes = useNodes(); const edges = useEdges(); @@ -98,39 +103,72 @@ function FuncNode({ data }: NodeProps>) { return; } - async function render() { + async function draw() { try { while (true) { const target = latestIndexRef.current; - let buffer: ArrayBuffer; + let result: RenderResult; - switch (renderMode) { + switch (render.renderMode) { case "Grayscale": - buffer = await renderGrayscale(name, target); + result = await renderGrayscale( + name, + target, + render.normalizationMode, + ); break; case "RGB": - buffer = await renderRgb(name, target); + result = await renderRgb(name, target, render.normalizationMode); break; case "Store Frequency": - buffer = await renderStoreFrequency(name, target); + result = await renderStoreFrequency( + name, + target, + render.normalizationMode, + width, + height, + ); break; case "Load Frequency": - buffer = await renderLoadFrequency(name, target); + result = await renderLoadFrequency( + name, + target, + render.normalizationMode, + width, + height, + ); break; case "Redundant Stores": - buffer = await renderRedundantStores(name, target); + result = await renderRedundantStores( + name, + target, + render.normalizationMode, + width, + height, + ); break; case "Reuse Distance": - buffer = await renderReuseDistance(name, target); + result = await renderReuseDistance( + name, + target, + render.normalizationMode, + width, + height, + ); break; } const ctx = canvasRef.current?.getContext("2d"); + // Update this Func's canvas with new pixel data. if (ctx) { - const pixels = new Uint8ClampedArray(buffer); - ctx.putImageData(new ImageData(pixels, width, height), 0, 0); + ctx.putImageData(new ImageData(result.pixels, width, height), 0, 0); + } + + // Update the histogram data for the currently active Func. + if (name === activeFunc) { + setHistogramData((prev) => ({ ...prev, data: result.histogram })); } if (latestIndexRef.current === target) { @@ -144,8 +182,8 @@ function FuncNode({ data }: NodeProps>) { } } - render(); - }, [packetIndex, name, width, height, renderMode]); + draw(); + }, [packetIndex, name, width, height, render, activeFunc, setHistogramData]); return ( <> diff --git a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx index 33469867c6ee..620341b92bfa 100644 --- a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -8,21 +8,18 @@ import LivenessControls from "@/components/controls/liveness/LivenessControls"; import PlaybackRate from "@/components/controls/playback/PlaybackRate"; import RenderMode from "@/components/controls/render/RenderMode"; import Histogram from "@/components/controls/histogram/Histogram"; -import HistogramSelect from "@/components/controls/histogram/HistogramSelect"; +import HistogramSelect from "@/components/controls/histogram/HistogramParameters"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; -import { histogramAtom, type HistogramScale } from "@/state/histogram"; -import { type RenderMode as RM, renderModeAtom } from "@/state/render"; -import { FuncMeta } from "@/types"; +import { histogramAtom } from "@/state/histogram"; +import { type RenderMode as RM, renderAtom } from "@/state/render"; -const RENDER_MODE_TO_HISTOGRAM_DATA_KEY: Record = { - Grayscale: "", - RGB: "", - "Store Frequency": "store_count_histogram", - "Load Frequency": "load_count_histogram", - "Redundant Stores": "redundant_count_histogram", - "Reuse Distance": "reuse_distance_histogram", -}; +const HISTOGRAM_RENDER_MODES = new Set([ + "Store Frequency", + "Load Frequency", + "Redundant Stores", + "Reuse Distance", +]); const RENDER_MODE_TO_LABEL: Record = { Grayscale: "", @@ -34,71 +31,64 @@ const RENDER_MODE_TO_LABEL: Record = { }; function VisualizationPanel() { - const { - funcs, - globalMaxStoreCount, - globalMaxLoadCount, - globalMaxRedundantCount, - globalMaxReuseDistance, - } = useTraceContext(); - const renderMode = useAtomValue(renderModeAtom); + const { funcs, globalMaxReuseDistance } = useTraceContext(); + const render = useAtomValue(renderAtom); const activeFunc = useAtomValue(funcAtom); - const histogramScale = useAtomValue(histogramAtom) as HistogramScale; + const { data, scale } = useAtomValue(histogramAtom); - const dataKey = RENDER_MODE_TO_HISTOGRAM_DATA_KEY[renderMode]; - const hasHistogram = dataKey && activeFunc && funcs[activeFunc]; - const domainMin = histogramScale === "log" ? 1 : 0; + const hasHistogram = + HISTOGRAM_RENDER_MODES.has(render.renderMode) && + activeFunc && + funcs[activeFunc] && + data !== null; + const domainMin = scale === "log" ? 1 : 0; const { data: histogramData, domain: histogramDomain } = React.useMemo((): { data: { x1: number; x2: number; y: number }[]; domain: [number, number]; } => { - if (!hasHistogram) { + if (!hasHistogram || !data) { return { data: [], domain: [domainMin, 1] }; } - const data = funcs[activeFunc][dataKey as keyof FuncMeta] as number[]; - - switch (renderMode) { + switch (render.renderMode) { case "Store Frequency": - return { - data: data.map((pixels, i) => ({ x1: i, x2: i + 1, y: pixels })), - domain: [domainMin, globalMaxStoreCount + 1], - }; case "Load Frequency": - return { - data: data.map((pixels, i) => ({ x1: i, x2: i + 1, y: pixels })), - domain: [domainMin, globalMaxLoadCount + 1], - }; case "Redundant Stores": return { - data: data.map((pixels, i) => ({ x1: i, x2: i + 1, y: pixels })), - domain: [domainMin, globalMaxRedundantCount + 1], + data: Array.from(data).map((y, i) => ({ + x1: i, + x2: i + 1, + y, + })), + domain: [domainMin, data.length], }; - // For Reuse Distance, scale x values to the global max reuse distance - // since the histogram is normalized to 64 bins. - case "Reuse Distance": + case "Reuse Distance": { + // For Reuse Distance, scale x values based on the normalization mode. + const domainMax = + render.normalizationMode === "Per Func" + ? funcs[activeFunc].max_reuse_distance + : globalMaxReuseDistance; + return { - data: data.map((pixels, i) => ({ - x1: Math.round((i / 64) * globalMaxReuseDistance), - x2: Math.round(((i + 1) / 64) * globalMaxReuseDistance), - y: pixels, + data: Array.from(data).map((y, i) => ({ + x1: Math.round((i / 64) * domainMax), + x2: Math.round(((i + 1) / 64) * domainMax), + y, })), - domain: [domainMin, globalMaxReuseDistance], + domain: [domainMin, domainMax], }; + } default: return { data: [], domain: [domainMin, 1] }; } }, [ hasHistogram, + data, activeFunc, funcs, - dataKey, - renderMode, + render, domainMin, - globalMaxStoreCount, - globalMaxLoadCount, - globalMaxRedundantCount, globalMaxReuseDistance, ]); @@ -116,7 +106,7 @@ function VisualizationPanel() {
diff --git a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx index 8a1b6b6a95bf..2d2ec7186c75 100644 --- a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx +++ b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx @@ -3,7 +3,7 @@ import * as d3 from "d3"; import { useAtomValue } from "jotai"; import * as React from "react"; -import { histogramAtom, HistogramScale } from "@/state/histogram"; +import { histogramAtom } from "@/state/histogram"; interface HistogramProps { data: { x1: number; x2: number; y: number }[]; @@ -14,9 +14,9 @@ interface HistogramProps { } function Histogram({ data, domain, labels }: HistogramProps) { - console.log("Data: ", data); const ref = React.useRef(null); - const histogramScale = useAtomValue(histogramAtom) as HistogramScale; + const { scale } = useAtomValue(histogramAtom); + // Build the data for the bottom colorbar. const colorbar = React.useMemo(() => { const range = domain[1] - domain[0]; @@ -53,7 +53,7 @@ function Histogram({ data, domain, labels }: HistogramProps) { tickFormat: (value) => d3.format(".2s")(value), tickPadding: 24, tickSize: 0, - type: histogramScale, + type: scale, interval: domain[1] <= 64 ? 1 : undefined, }, color: { @@ -81,7 +81,7 @@ function Histogram({ data, domain, labels }: HistogramProps) { return () => { plot.remove(); }; - }, [data, domain, labels, histogramScale, colorbar]); + }, [data, domain, labels, scale, colorbar]); return data.every((d) => d.y === 0) ? (
diff --git a/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx b/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx new file mode 100644 index 000000000000..c66fc8bced32 --- /dev/null +++ b/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx @@ -0,0 +1,175 @@ +import { Label, Select } from "radix-ui"; +import { useAtom } from "jotai"; + +import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; +import { useTraceContext } from "@/hooks/trace"; +import { funcAtom } from "@/state/func"; +import { histogramAtom, type HistogramScale } from "@/state/histogram"; +import { type NormalizationMode, renderAtom } from "@/state/render"; + +function HistogramSelect() { + const { funcs } = useTraceContext(); + const [activeFunc, setActiveFunc] = useAtom(funcAtom); + const [histogram, setHistogram] = useAtom(histogramAtom); + const [render, setRender] = useAtom(renderAtom); + + return ( +
+
+ + Selected Func + + { + setActiveFunc(value); + setHistogram({ ...histogram, data: null }); + }} + > + + + + + + + + + + + + + {Object.keys(funcs).map((func) => ( + + + {func} + + + ))} + + + +
+
+
+ + Scale + + + setHistogram({ ...histogram, scale: value as HistogramScale }) + } + > + + + + + + + + + + Linear + + + Log + + + + +
+
+ + Normalize Display + + + setRender({ + ...render, + normalizationMode: value as NormalizationMode, + }) + } + > + + + + + + + + + + Across Funcs + + + Per Func + + + + +
+
+
+ ); +} + +export default HistogramSelect; diff --git a/apps/halidoscope/src/components/controls/histogram/HistogramSelect.tsx b/apps/halidoscope/src/components/controls/histogram/HistogramSelect.tsx deleted file mode 100644 index eda07d9ec204..000000000000 --- a/apps/halidoscope/src/components/controls/histogram/HistogramSelect.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Label, Select } from "radix-ui"; -import { useAtom } from "jotai"; - -import { useTraceContext } from "@/hooks/trace"; -import { funcAtom } from "@/state/func"; -import { histogramAtom, type HistogramScale } from "@/state/histogram"; - -function HistogramSelect() { - const { funcs } = useTraceContext(); - const [activeFunc, setActiveFunc] = useAtom(funcAtom); - const [histogramScale, setHistogramScale] = useAtom(histogramAtom); - - return ( -
-
- - Selected Func - - setActiveFunc(value)} - > - - - - - - - - - - - - - {Object.keys(funcs).map((func) => ( - - - {func} - - - ))} - - - -
-
- - Scale - - setHistogramScale(value as HistogramScale)} - > - - - - - - - - - - - - Linear - - - Log - - - - -
-
- ); -} - -export default HistogramSelect; diff --git a/apps/halidoscope/src/components/controls/render/RenderMode.tsx b/apps/halidoscope/src/components/controls/render/RenderMode.tsx index bfc3011bb249..7fa1db76684c 100644 --- a/apps/halidoscope/src/components/controls/render/RenderMode.tsx +++ b/apps/halidoscope/src/components/controls/render/RenderMode.tsx @@ -1,15 +1,17 @@ import { Select } from "radix-ui"; import { useAtom } from "jotai"; -import { renderModeAtom, RENDER_MODES, type RenderMode } from "@/state/render"; +import { renderAtom, RENDER_MODES, type RenderMode } from "@/state/render"; function VisualizationSelect() { - const [renderMode, setVisualizationMode] = useAtom(renderModeAtom); + const [render, setRender] = useAtom(renderAtom); return ( setVisualizationMode(value as RenderMode)} + value={render.renderMode} + onValueChange={(value) => + setRender({ ...render, renderMode: value as RenderMode }) + } > + + + ); +} + +export default ArrowDownIcon; diff --git a/apps/halidoscope/src/hooks/trace.ts b/apps/halidoscope/src/hooks/trace.ts index 1cf16c0577a3..15b3181999a6 100644 --- a/apps/halidoscope/src/hooks/trace.ts +++ b/apps/halidoscope/src/hooks/trace.ts @@ -6,17 +6,11 @@ const TraceContext = React.createContext<{ funcs: Record; dagEdges: Record; packetCount: number; - globalMaxStoreCount: number; - globalMaxLoadCount: number; - globalMaxRedundantCount: number; globalMaxReuseDistance: number; }>({ funcs: {}, dagEdges: {}, packetCount: 0, - globalMaxStoreCount: 0, - globalMaxLoadCount: 0, - globalMaxRedundantCount: 0, globalMaxReuseDistance: 0, }); diff --git a/apps/halidoscope/src/state/histogram.ts b/apps/halidoscope/src/state/histogram.ts index c483deb34013..a6c3f1f2a1df 100644 --- a/apps/halidoscope/src/state/histogram.ts +++ b/apps/halidoscope/src/state/histogram.ts @@ -2,4 +2,10 @@ import { atom } from "jotai"; export type HistogramScale = "linear" | "log"; -export const histogramAtom = atom("linear"); +export const histogramAtom = atom<{ + data: Uint32Array | null; + scale: HistogramScale; +}>({ + data: null, + scale: "linear", +}); diff --git a/apps/halidoscope/src/state/render.ts b/apps/halidoscope/src/state/render.ts index 9c388c89ac65..a33da211f6f7 100644 --- a/apps/halidoscope/src/state/render.ts +++ b/apps/halidoscope/src/state/render.ts @@ -9,5 +9,9 @@ export const RENDER_MODES = [ "Reuse Distance", ] as const; export type RenderMode = (typeof RENDER_MODES)[number]; +export type NormalizationMode = "Across Funcs" | "Per Func"; -export const renderModeAtom = atom("Grayscale"); +export const renderAtom = atom<{ + renderMode: RenderMode; + normalizationMode: NormalizationMode; +}>({ renderMode: "Grayscale", normalizationMode: "Across Funcs" }); diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts index b0a7e73ee913..268e01ed1733 100644 --- a/apps/halidoscope/src/types/index.ts +++ b/apps/halidoscope/src/types/index.ts @@ -23,10 +23,6 @@ export interface FuncMeta extends Record { max_load_count: number; max_redundant_count: number; max_reuse_distance: number; - store_count_histogram: number[]; - load_count_histogram: number[]; - redundant_count_histogram: number[]; - reuse_distance_histogram: number[]; buffer_liveness: IndexRange; produce_ranges: IndexRange[]; consume_ranges: IndexRange[]; @@ -38,9 +34,6 @@ export interface TraceMeta { funcs: FuncMeta[]; total_packets: number; dag_edges: Record; - global_max_store_count: number; - global_max_load_count: number; - global_max_redundant_count: number; global_max_reuse_distance: number; } diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index 0f6721e02cf0..b51a2602fedc 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -1,7 +1,13 @@ import { invoke } from "@tauri-apps/api/core"; +import type { NormalizationMode } from "@/state/render"; import type { TraceMeta } from "@/types"; +export interface RenderResult { + pixels: Uint8ClampedArray; + histogram: Uint32Array | null; +} + export async function openTrace(path: string): Promise { return invoke("open_trace", { path }); } @@ -9,41 +15,106 @@ export async function openTrace(path: string): Promise { export async function renderGrayscale( func: string, globalIndex: number, -): Promise { - return invoke("render_grayscale", { func, globalIndex }); + normalizationMode: NormalizationMode, +): Promise { + const buffer = await invoke("render_grayscale", { + func, + globalIndex, + normalizationMode, + }); + + return { pixels: new Uint8ClampedArray(buffer), histogram: null }; } export async function renderRgb( func: string, globalIndex: number, -): Promise { - return invoke("render_rgb", { func, globalIndex }); + normalizationMode: NormalizationMode, +): Promise { + const buffer = await invoke("render_rgb", { + func, + globalIndex, + normalizationMode, + }); + + return { pixels: new Uint8ClampedArray(buffer), histogram: null }; +} + +// The backend appends the histogram's bins as little-endian u32s directly after the pixel +// bytes in a single response; split at the known pixel-buffer length to recover both. +function splitPixelsAndHistogram( + buffer: ArrayBuffer, + width: number, + height: number, +): RenderResult { + const pixelByteLength = width * height * 4; + + return { + pixels: new Uint8ClampedArray(buffer, 0, pixelByteLength), + histogram: new Uint32Array(buffer, pixelByteLength), + }; } export async function renderStoreFrequency( func: string, globalIndex: number, -): Promise { - return invoke("render_store_frequency", { func, globalIndex }); + normalizationMode: NormalizationMode, + width: number, + height: number, +): Promise { + const buffer = await invoke("render_store_frequency", { + func, + globalIndex, + normalizationMode, + }); + + return splitPixelsAndHistogram(buffer, width, height); } export async function renderLoadFrequency( func: string, globalIndex: number, -): Promise { - return invoke("render_load_frequency", { func, globalIndex }); + normalizationMode: NormalizationMode, + width: number, + height: number, +): Promise { + const buffer = await invoke("render_load_frequency", { + func, + globalIndex, + normalizationMode, + }); + + return splitPixelsAndHistogram(buffer, width, height); } export async function renderRedundantStores( func: string, globalIndex: number, -): Promise { - return invoke("render_redundant_stores", { func, globalIndex }); + normalizationMode: NormalizationMode, + width: number, + height: number, +): Promise { + const buffer = await invoke("render_redundant_stores", { + func, + globalIndex, + normalizationMode, + }); + + return splitPixelsAndHistogram(buffer, width, height); } export async function renderReuseDistance( func: string, globalIndex: number, -): Promise { - return invoke("render_reuse_distance", { func, globalIndex }); + normalizationMode: NormalizationMode, + width: number, + height: number, +): Promise { + const buffer = await invoke("render_reuse_distance", { + func, + globalIndex, + normalizationMode, + }); + + return splitPixelsAndHistogram(buffer, width, height); } From c69dad22be992b4aa5b9b24d798b2e1b0ad2d121 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Mon, 13 Jul 2026 14:09:25 -0700 Subject: [PATCH 23/67] Add list and stats CLI subcommands. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/src-tauri/Cargo.lock | 56 +++++++ apps/halidoscope/src-tauri/Cargo.toml | 1 + apps/halidoscope/src-tauri/src/cli.rs | 162 ++++++++++++++++++++- apps/halidoscope/src-tauri/src/lib.rs | 8 +- apps/halidoscope/src-tauri/tauri.conf.json | 39 +++++ 5 files changed, 259 insertions(+), 7 deletions(-) diff --git a/apps/halidoscope/src-tauri/Cargo.lock b/apps/halidoscope/src-tauri/Cargo.lock index 7d78f17b5c77..70fa7ccff162 100644 --- a/apps/halidoscope/src-tauri/Cargo.lock +++ b/apps/halidoscope/src-tauri/Cargo.lock @@ -564,6 +564,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "comfy-table" +version = "7.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -656,6 +667,29 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.12.1", + "crossterm_winapi", + "document-features", + "parking_lot", + "rustix", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -858,6 +892,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dom_query" version = "0.27.0" @@ -1496,6 +1539,7 @@ name = "halidoscope" version = "0.1.0" dependencies = [ "colorous", + "comfy-table", "serde", "serde_json", "tauri", @@ -2047,6 +2091,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -4131,6 +4181,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/apps/halidoscope/src-tauri/Cargo.toml b/apps/halidoscope/src-tauri/Cargo.toml index d436f7a2af86..57572c6d0563 100644 --- a/apps/halidoscope/src-tauri/Cargo.toml +++ b/apps/halidoscope/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" colorous = "1.0.16" +comfy-table = "7" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-cli = "2.0.0" diff --git a/apps/halidoscope/src-tauri/src/cli.rs b/apps/halidoscope/src-tauri/src/cli.rs index 6ee6ff77773c..394beaa14bec 100644 --- a/apps/halidoscope/src-tauri/src/cli.rs +++ b/apps/halidoscope/src-tauri/src/cli.rs @@ -1,8 +1,11 @@ use std::ffi::OsStr; use std::path::Path; +use comfy_table::{presets, Table}; +use serde_json::json; use tauri_plugin_cli::SubcommandMatches; +use crate::commands::TraceMeta; use crate::graph::to_dot; use crate::trace::Trace; @@ -11,7 +14,7 @@ pub fn halidoscope_cli(subcommand: Box) { "snapshot" => { let args = &subcommand.matches.args; - let trace = args + let trace_path = args .get("trace") .and_then(|a| a.value.as_str()) .unwrap_or_else(|| { @@ -43,27 +46,27 @@ pub fn halidoscope_cli(subcommand: Box) { }); // Load and parse the trace. - let tr = Trace::load_from_file(trace).unwrap_or_else(|e| { + let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { eprintln!("Error loading trace: {}", e); std::process::exit(1); }); // Find the target function. - let _target_func = tr.funcs.get(func).unwrap_or_else(|| { + let _target_func = trace.funcs.get(func).unwrap_or_else(|| { eprintln!( "Func '{}' not found in trace. Available Funcs: {:?}", func, - tr.funcs.keys().collect::>() + trace.funcs.keys().collect::>() ); std::process::exit(1); }); // Exit early if the packet index is out of bounds. - if packet_index as usize >= tr.packets.len() { + if packet_index as usize >= trace.packets.len() { eprintln!( "Packet index {} is out of bounds. Valid range: 0..{}", packet_index, - tr.packets.len() + trace.packets.len() ); std::process::exit(1); } @@ -118,6 +121,153 @@ pub fn halidoscope_cli(subcommand: Box) { } } } + "list" => { + let args = &subcommand.matches.args; + + let trace_path = args + .get("trace") + .and_then(|a| a.value.as_str()) + .unwrap_or_else(|| { + eprintln!("Error: --trace argument is required."); + std::process::exit(1); + }); + + // Load and parse the trace. + let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + eprintln!("Error loading trace: {}", e); + std::process::exit(1); + }); + + let as_json = args + .get("json") + .and_then(|a| a.value.as_bool()) + .unwrap_or(false); + + println!("Printing Funcs for {}", trace_path); + + if as_json { + let data = &trace + .funcs + .iter() + .map(|(name, func)| { + json!({ + "func": name.clone(), + "dimensionality": func.min_coords.len() + }) + }) + .collect::>(); + let json = serde_json::to_string_pretty(&data).unwrap_or_else(|err| { + eprintln!("Error serializing Funcs to JSON: {}", err); + std::process::exit(1); + }); + println!("{}", json); + } else { + let mut table = Table::new(); + table.set_header(vec!["Func", "Dimensionality"]); + for (func_name, func) in &trace.funcs { + table.add_row(vec![func_name.clone(), func.min_coords.len().to_string()]); + } + println!("{table}"); + } + + std::process::exit(0); + } + "stats" => { + let args = &subcommand.matches.args; + + let trace_path = args + .get("trace") + .and_then(|a| a.value.as_str()) + .unwrap_or_else(|| { + eprintln!("Error: --trace argument is required."); + std::process::exit(1); + }); + let func_filter = args.get("func").and_then(|a| a.value.as_str()); + + // Load and parse the trace. + let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + eprintln!("Error loading trace: {}", e); + std::process::exit(1); + }); + + // Reuse the same derivation the GUI uses so the CLI reports identical stats. + let meta = TraceMeta::from_trace(&trace); + + let funcs: Vec<_> = match func_filter { + Some(name) => { + let matched: Vec<_> = meta.funcs.iter().filter(|f| f.name == name).collect(); + if matched.is_empty() { + eprintln!( + "Func '{}' not found in trace. Available Funcs: {:?}", + name, + meta.funcs.iter().map(|f| &f.name).collect::>() + ); + std::process::exit(1); + } + matched + } + None => meta.funcs.iter().collect(), + }; + + let as_json = args + .get("json") + .and_then(|a| a.value.as_bool()) + .unwrap_or(false); + + println!("Printing Func statistics for {}", trace_path); + + if as_json { + let json = serde_json::to_string_pretty(&funcs).unwrap_or_else(|err| { + eprintln!("Error serializing Func statistics to JSON: {}", err); + std::process::exit(1); + }); + println!("{}", json); + } else { + let mut table = Table::new(); + table + .load_preset(presets::UTF8_HORIZONTAL_ONLY) + .set_header(vec![ + "Func", + "Minimum Coordinates", + "Maximum Coordinates", + "Minimum Value", + "Maximum Value", + "Maximum Store Count", + "Maximum Load Count", + "Thread Count", + ]); + + for func in funcs { + table.add_row(vec![ + func.name.clone(), + format!( + "({})", + func.min_coords + .iter() + .map(ToString::to_string) + .collect::>() + .join(",") + ), + format!( + "({})", + func.max_coords + .iter() + .map(ToString::to_string) + .collect::>() + .join(",") + ), + func.min_value.map(|v| v.to_string()).unwrap_or_default(), + func.max_value.map(|v| v.to_string()).unwrap_or_default(), + func.max_store_count.to_string(), + func.max_load_count.to_string(), + func.thread_count.to_string(), + ]); + } + println!("{table}"); + } + + std::process::exit(0); + } cmd => { eprintln!("Unknown subcommand {}", cmd); std::process::exit(1); diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index 8d49242e8cc9..bb568cda51f0 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -27,7 +27,13 @@ pub fn run() { match app.cli().matches() { Ok(matches) => match matches.subcommand { Some(subcommand) => halidoscope_cli(subcommand), - None => {} + None => { + tauri::WebviewWindowBuilder::from_config( + app.handle(), + &app.config().app.windows[0], + )? + .build()?; + } }, Err(e) => { eprintln!("Error parsing CLI arguments: {}", e); diff --git a/apps/halidoscope/src-tauri/tauri.conf.json b/apps/halidoscope/src-tauri/tauri.conf.json index b767a788f080..2f1266e49963 100644 --- a/apps/halidoscope/src-tauri/tauri.conf.json +++ b/apps/halidoscope/src-tauri/tauri.conf.json @@ -12,6 +12,7 @@ "app": { "windows": [ { + "create": false, "title": "Halidoscope", "width": 1512, "height": 982 @@ -104,6 +105,44 @@ "description": "Path to write the output snapshot image." } ] + }, + "list": { + "args": [ + { + "name": "trace", + "short": "t", + "takesValue": true, + "description": "Path to .hltrace file to analyze for pipeline graph structure.", + "required": true + }, + { + "name": "json", + "takesValue": false, + "description": "Print Func names and dimensionality in JSON format." + } + ] + }, + "stats": { + "args": [ + { + "name": "trace", + "short": "t", + "takesValue": true, + "description": "Path to .hltrace file to analyze for Func statistics.", + "required": true + }, + { + "name": "func", + "short": "f", + "takesValue": true, + "description": "Name of the Func to print statistics for. If omitted, prints statistics for all Funcs." + }, + { + "name": "json", + "takesValue": false, + "description": "Print statistics in JSON format." + } + ] } } } From c8fd2032bcf1ba5780bb699c81514cbdf2efcdbf Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 14 Jul 2026 12:37:54 -0700 Subject: [PATCH 24/67] Adjust halidoscope snapshot subcommand to return values as JSON. Co-authored-by: Claude Opus 4.8 --- apps/halidoscope/src-tauri/src/cli.rs | 540 ++++++++++++--------- apps/halidoscope/src-tauri/src/commands.rs | 12 +- apps/halidoscope/src-tauri/src/render.rs | 90 +++- apps/halidoscope/src-tauri/src/trace.rs | 30 +- 4 files changed, 400 insertions(+), 272 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/cli.rs b/apps/halidoscope/src-tauri/src/cli.rs index 394beaa14bec..ab6badc712ff 100644 --- a/apps/halidoscope/src-tauri/src/cli.rs +++ b/apps/halidoscope/src-tauri/src/cli.rs @@ -1,275 +1,353 @@ use std::ffi::OsStr; use std::path::Path; +use comfy_table::presets::UTF8_HORIZONTAL_ONLY; use comfy_table::{presets, Table}; use serde_json::json; use tauri_plugin_cli::SubcommandMatches; use crate::commands::TraceMeta; use crate::graph::to_dot; +use crate::render::{ + GrayscaleState, LoadFrequencyState, RedundantState, Renderer, ReuseDistanceState, RgbState, + StoreFrequencyState, +}; use crate::trace::Trace; pub fn halidoscope_cli(subcommand: Box) { match subcommand.name.as_str() { - "snapshot" => { - let args = &subcommand.matches.args; - - let trace_path = args - .get("trace") - .and_then(|a| a.value.as_str()) - .unwrap_or_else(|| { - eprintln!("Error: --trace argument is required."); - std::process::exit(1); - }); - let func = args - .get("func") - .and_then(|a| a.value.as_str()) - .unwrap_or_else(|| { - eprintln!("Error: --func argument is required."); - std::process::exit(1); - }); - let packet_index = args - .get("packet-index") - .and_then(|a| a.value.as_str()) - .and_then(|s| s.parse::().ok()) - .unwrap_or(0); - let _mode = args - .get("mode") - .and_then(|a| a.value.as_str()) - .unwrap_or("grayscale"); - let _destination = args - .get("destination") - .and_then(|a| a.value.as_str()) - .unwrap_or_else(|| { - eprintln!("Error: A destination for the snapshot is required."); + "dot" => dot(subcommand), + "list" => list(subcommand), + "snapshot" => snapshot(subcommand), + "stats" => stats(subcommand), + cmd => { + eprintln!("Unknown subcommand {}", cmd); + std::process::exit(1); + } + }; +} + +fn dot(subcommand: Box) -> Option<()> { + let args = &subcommand.matches.args; + + let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; + let destination = args.get("destination").and_then(|a| a.value.as_str()); + + // Load and parse the trace. + let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + eprintln!("Error loading trace: {}", e); + std::process::exit(1); + }); + let dot = to_dot(&trace.dag_edges); + + match destination { + Some(dest) => { + let ext = Path::new(dest).extension().and_then(OsStr::to_str); + + match ext { + Some("txt") | Some("gv") | Some("dot") => { + if let Err(e) = std::fs::write(dest, &dot) { + eprintln!("Failed to write DOT file: {}", e); + std::process::exit(1); + } + + println!("DOT file written to {}", dest); + std::process::exit(0); + } + _ => { + eprintln!( + "Unsupported file extension for DOT file, must be one of .txt, .gv, or .dot." + ); std::process::exit(1); - }); + } + } + } + None => { + println!("{}", dot); + std::process::exit(0); + } + } +} - // Load and parse the trace. - let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { - eprintln!("Error loading trace: {}", e); - std::process::exit(1); - }); +fn list(subcommand: Box) -> Option<()> { + let args = &subcommand.matches.args; - // Find the target function. - let _target_func = trace.funcs.get(func).unwrap_or_else(|| { - eprintln!( - "Func '{}' not found in trace. Available Funcs: {:?}", - func, - trace.funcs.keys().collect::>() - ); - std::process::exit(1); - }); + let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; + + // Load and parse the trace. + let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + eprintln!("Error loading trace: {}", e); + std::process::exit(1); + }); + + let as_json = args + .get("json") + .and_then(|a| a.value.as_bool()) + .unwrap_or(false); + + println!("Printing Funcs for {}", trace_path); + + if as_json { + let data = &trace + .funcs + .iter() + .map(|(name, func)| { + json!({ + "func": name.clone(), + "dimensionality": func.min_coords.len() + }) + }) + .collect::>(); + + let json = serde_json::to_string_pretty(&data).unwrap_or_else(|err| { + eprintln!("Error serializing Funcs to JSON: {}", err); + std::process::exit(1); + }); + + println!("{}", json); + } else { + let mut table = Table::new(); + + table + .load_preset(UTF8_HORIZONTAL_ONLY) + .set_header(vec!["Func", "Dimensionality"]); + + for (func_name, func) in &trace.funcs { + table.add_row(vec![func_name.clone(), func.min_coords.len().to_string()]); + } + + println!("{table}"); + } + + std::process::exit(0); +} + +fn stats(subcommand: Box) -> Option<()> { + let args = &subcommand.matches.args; + + let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; + let func = args.get("func").and_then(|a| a.value.as_str()); + + // Load and parse the trace. + let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + eprintln!("Error loading trace: {}", e); + std::process::exit(1); + }); + + // Reuse the same derivation the GUI uses so the CLI reports identical stats. + let meta = TraceMeta::from_trace(&trace); - // Exit early if the packet index is out of bounds. - if packet_index as usize >= trace.packets.len() { + let funcs: Vec<_> = match func { + Some(name) => { + let matched: Vec<_> = meta.funcs.iter().filter(|f| f.name == name).collect(); + + if matched.is_empty() { eprintln!( - "Packet index {} is out of bounds. Valid range: 0..{}", - packet_index, - trace.packets.len() + "Func '{}' not found in trace. Available Funcs: {:?}", + name, + meta.funcs.iter().map(|f| &f.name).collect::>() ); std::process::exit(1); } - // TODO: Convert this command to write out numeric values to CSV / JSON. - std::process::exit(0); + matched } - "dot" => { - let args = &subcommand.matches.args; - - let trace = args - .get("trace") - .and_then(|a| a.value.as_str()) - .unwrap_or_else(|| { - eprintln!("Error: --trace argument is required."); - std::process::exit(1); - }); - let destination = args.get("destination").and_then(|a| a.value.as_str()); + None => meta.funcs.iter().collect(), + }; - // Load and parse the trace. - let tr = Trace::load_from_file(trace).unwrap_or_else(|e| { - eprintln!("Error loading trace: {}", e); - std::process::exit(1); - }); - let dot = to_dot(&tr.dag_edges); - - match destination { - Some(dest) => { - let ext = Path::new(dest).extension().and_then(OsStr::to_str); - - match ext { - Some("txt") | Some("gv") | Some("dot") => { - if let Err(e) = std::fs::write(dest, &dot) { - eprintln!("Failed to write DOT file: {}", e); - std::process::exit(1); - } - - println!("DOT file written to {}", dest); - std::process::exit(0); - } - _ => { - eprintln!( - "Unsupported file extension for DOT file, must be one of .txt, .gv, or .dot." - ); - std::process::exit(1); - } - } - } - None => { - println!("{}", dot); - std::process::exit(0); - } - } + let as_json = args.get("json").and_then(|a| a.value.as_bool())?; + + println!("Printing Func statistics for {}", trace_path); + + if as_json { + let json = serde_json::to_string_pretty(&funcs).unwrap_or_else(|err| { + eprintln!("Error serializing Func statistics to JSON: {}", err); + std::process::exit(1); + }); + println!("{}", json); + } else { + let mut table = Table::new(); + + table + .load_preset(presets::UTF8_HORIZONTAL_ONLY) + .set_header(vec![ + "Func", + "Minimum Coordinates", + "Maximum Coordinates", + "Minimum Value", + "Maximum Value", + "Maximum Store Count", + "Maximum Load Count", + "Thread Count", + ]); + + for func in funcs { + table.add_row(vec![ + func.name.clone(), + format!( + "({})", + func.min_coords + .iter() + .map(ToString::to_string) + .collect::>() + .join(",") + ), + format!( + "({})", + func.max_coords + .iter() + .map(ToString::to_string) + .collect::>() + .join(",") + ), + func.min_value.map(|v| v.to_string()).unwrap_or_default(), + func.max_value.map(|v| v.to_string()).unwrap_or_default(), + func.max_store_count.to_string(), + func.max_load_count.to_string(), + func.thread_count.to_string(), + ]); } - "list" => { - let args = &subcommand.matches.args; - - let trace_path = args - .get("trace") - .and_then(|a| a.value.as_str()) - .unwrap_or_else(|| { - eprintln!("Error: --trace argument is required."); - std::process::exit(1); - }); + println!("{table}"); + } - // Load and parse the trace. - let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { - eprintln!("Error loading trace: {}", e); - std::process::exit(1); - }); - - let as_json = args - .get("json") - .and_then(|a| a.value.as_bool()) - .unwrap_or(false); - - println!("Printing Funcs for {}", trace_path); - - if as_json { - let data = &trace - .funcs - .iter() - .map(|(name, func)| { - json!({ - "func": name.clone(), - "dimensionality": func.min_coords.len() - }) + std::process::exit(0); +} + +fn snapshot(subcommand: Box) -> Option<()> { + let args = &subcommand.matches.args; + + let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; + let func = args.get("func").and_then(|a| a.value.as_str())?; + let packet_index = args + .get("packet-index") + .and_then(|a| a.value.as_str()) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + let mode = args + .get("mode") + .and_then(|a| a.value.as_str()) + .unwrap_or("grayscale"); + let destination = args.get("destination").and_then(|a| a.value.as_str())?; + + // Load and parse the trace. + let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + eprintln!("Error loading trace: {}", e); + std::process::exit(1); + }); + + // Find the target function. + let target_func = trace.funcs.get(func)?; + + // Exit early if the packet index is out of bounds. + if packet_index as usize >= trace.packets.len() { + eprintln!( + "Packet index {} is out of bounds. Valid range: 0..{}", + packet_index, + trace.packets.len() + ); + std::process::exit(1); + } + + let ext = Path::new(destination).extension().and_then(OsStr::to_str); + let store_indices = trace.func_store_indices(&func)?; + let load_indices = trace.func_load_indices(&func)?; + let k = packet_index.try_into().unwrap_or_else(|_| { + eprintln!("Packet index {} is too large.", packet_index); + std::process::exit(1); + }); + + match ext { + Some("json") => { + // Obtain the values for the given rendering mode at the current packet index. + let json = match mode { + "grayscale" => { + let Some(mut gs) = GrayscaleState::new(&trace, &target_func.name) else { + eprintln!("Func '{}' has no usable geometry.", target_func.name); + std::process::exit(1); + }; + + gs.seek(&trace, store_indices, k); + serde_json::to_string_pretty(&gs.to_values()).unwrap_or_else(|e| { + eprintln!("Error serializing values to JSON: {}", e); + std::process::exit(1); }) - .collect::>(); - let json = serde_json::to_string_pretty(&data).unwrap_or_else(|err| { - eprintln!("Error serializing Funcs to JSON: {}", err); - std::process::exit(1); - }); - println!("{}", json); - } else { - let mut table = Table::new(); - table.set_header(vec!["Func", "Dimensionality"]); - for (func_name, func) in &trace.funcs { - table.add_row(vec![func_name.clone(), func.min_coords.len().to_string()]); } - println!("{table}"); - } + "rgb" => { + let Some(mut rgbs) = RgbState::new(&trace, &target_func.name) else { + eprintln!("Func '{}' has no usable geometry.", target_func.name); + std::process::exit(1); + }; - std::process::exit(0); - } - "stats" => { - let args = &subcommand.matches.args; - - let trace_path = args - .get("trace") - .and_then(|a| a.value.as_str()) - .unwrap_or_else(|| { - eprintln!("Error: --trace argument is required."); - std::process::exit(1); - }); - let func_filter = args.get("func").and_then(|a| a.value.as_str()); + rgbs.seek(&trace, store_indices, k); + serde_json::to_string_pretty(&rgbs.to_values()).unwrap_or_else(|e| { + eprintln!("Error serializing values to JSON: {}", e); + std::process::exit(1); + }) + } + "store-frequency" => { + let Some(mut sfs) = StoreFrequencyState::new(&trace, &target_func.name) else { + eprintln!("Func '{}' has no usable geometry.", target_func.name); + std::process::exit(1); + }; - // Load and parse the trace. - let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { - eprintln!("Error loading trace: {}", e); - std::process::exit(1); - }); - - // Reuse the same derivation the GUI uses so the CLI reports identical stats. - let meta = TraceMeta::from_trace(&trace); - - let funcs: Vec<_> = match func_filter { - Some(name) => { - let matched: Vec<_> = meta.funcs.iter().filter(|f| f.name == name).collect(); - if matched.is_empty() { - eprintln!( - "Func '{}' not found in trace. Available Funcs: {:?}", - name, - meta.funcs.iter().map(|f| &f.name).collect::>() - ); + sfs.seek(&trace, store_indices, k); + serde_json::to_string_pretty(&sfs.to_values()).unwrap_or_else(|e| { + eprintln!("Error serializing values to JSON: {}", e); std::process::exit(1); - } - matched + }) } - None => meta.funcs.iter().collect(), - }; + "load-frequency" => { + let Some(mut lfs) = LoadFrequencyState::new(&trace, &target_func.name) else { + eprintln!("Func '{}' has no usable geometry.", target_func.name); + std::process::exit(1); + }; - let as_json = args - .get("json") - .and_then(|a| a.value.as_bool()) - .unwrap_or(false); + lfs.seek(&trace, load_indices, k); + serde_json::to_string_pretty(&lfs.to_values()).unwrap_or_else(|e| { + eprintln!("Error serializing values to JSON: {}", e); + std::process::exit(1); + }) + } + "redundant-stores" => { + let Some(mut rs) = RedundantState::new(&trace, &target_func.name) else { + eprintln!("Func '{}' has no usable geometry.", target_func.name); + std::process::exit(1); + }; - println!("Printing Func statistics for {}", trace_path); + rs.seek(&trace, store_indices, k); + serde_json::to_string_pretty(&rs.to_values()).unwrap_or_else(|e| { + eprintln!("Error serializing values to JSON: {}", e); + std::process::exit(1); + }) + } + "reuse-distance" => { + let Some(mut rds) = ReuseDistanceState::new(&trace, &target_func.name) else { + eprintln!("Func '{}' has no usable geometry.", target_func.name); + std::process::exit(1); + }; - if as_json { - let json = serde_json::to_string_pretty(&funcs).unwrap_or_else(|err| { - eprintln!("Error serializing Func statistics to JSON: {}", err); + rds.seek(&trace, store_indices, load_indices, k, k); + serde_json::to_string_pretty(&rds.to_values()).unwrap_or_else(|e| { + eprintln!("Error serializing values to JSON: {}", e); + std::process::exit(1); + }) + } + _ => { + eprintln!("Unsupported rendering mode '{}'.", mode); std::process::exit(1); - }); - println!("{}", json); - } else { - let mut table = Table::new(); - table - .load_preset(presets::UTF8_HORIZONTAL_ONLY) - .set_header(vec![ - "Func", - "Minimum Coordinates", - "Maximum Coordinates", - "Minimum Value", - "Maximum Value", - "Maximum Store Count", - "Maximum Load Count", - "Thread Count", - ]); - - for func in funcs { - table.add_row(vec![ - func.name.clone(), - format!( - "({})", - func.min_coords - .iter() - .map(ToString::to_string) - .collect::>() - .join(",") - ), - format!( - "({})", - func.max_coords - .iter() - .map(ToString::to_string) - .collect::>() - .join(",") - ), - func.min_value.map(|v| v.to_string()).unwrap_or_default(), - func.max_value.map(|v| v.to_string()).unwrap_or_default(), - func.max_store_count.to_string(), - func.max_load_count.to_string(), - func.thread_count.to_string(), - ]); } - println!("{table}"); + }; + + if let Err(e) = std::fs::write(destination, json) { + eprintln!("Failed to write snapshot file: {}", e); + std::process::exit(1); } + println!("Snapshot written to {}", destination); std::process::exit(0); } - cmd => { - eprintln!("Unknown subcommand {}", cmd); + _ => { + eprintln!("Unsupported file extension for snapshot file, must be .json."); std::process::exit(1); } } diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 90460fe7365d..3a5b722d2504 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -40,10 +40,10 @@ pub struct FuncMeta { pub max_coords: Vec, pub min_value: Option, pub max_value: Option, - pub max_store_count: i32, - pub max_load_count: i32, - pub max_redundant_count: i32, - pub max_reuse_distance: i64, + pub max_store_count: u32, + pub max_load_count: u32, + pub max_redundant_count: u32, + pub max_reuse_distance: u64, pub buffer_liveness: IndexRange, pub produce_ranges: Vec, pub consume_ranges: Vec, @@ -56,7 +56,7 @@ pub struct TraceMeta { pub funcs: Vec, pub total_packets: u32, pub dag_edges: BTreeMap>, - pub global_max_reuse_distance: i64, + pub global_max_reuse_distance: u64, } impl TraceMeta { @@ -64,7 +64,7 @@ impl TraceMeta { /// are still listed (with zero dimensions) so the UI can surface them; the renderer simply /// produces nothing for them. pub fn from_trace(trace: &Trace) -> Self { - let mut global_max_reuse_distance = 0i64; + let mut global_max_reuse_distance = 0u64; let funcs = trace .funcs diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index e3c3172bb86f..674bec7e5452 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -19,9 +19,14 @@ pub enum NormalizationMode { // A trait that all 2D rendering states implement. pub trait Renderer: Sized { + /// The primitive type this state's `values` are stored and returned as (e.g. `f64` for + /// intensity-accumulating states, `u32`/`i32` for count-accumulating states). + type Value; + fn register(trace: &Trace, func: &str) -> Option; fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize); fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec; + fn to_values(&self) -> Vec; } // ── Grayscale rendering ─────────────────────────────────────────────────────── @@ -35,6 +40,7 @@ pub struct GrayscaleState { /// Latest normalized intensity per (pixel, channel), row-major with channel as the minor axis. /// Length is `width * height * channels`. Unwritten cells stay 0. framebuffer: Vec, + values: Vec, applied_k: usize, } @@ -45,11 +51,14 @@ impl GrayscaleState { let min_v = stats.min_value.unwrap_or(0.0); let max_v = stats.max_value.unwrap_or(255.0); let framebuffer = vec![0u8; geom.width * geom.height * geom.channels]; + let values = vec![0f64; geom.width * geom.height * geom.channels]; + Some(Self { geom, min_v, max_v, framebuffer, + values, applied_k: 0, }) } @@ -84,6 +93,7 @@ impl GrayscaleState { } let idx = (y as usize * width + x as usize) * channels + c as usize; self.framebuffer[idx] = self.normalize(v); + self.values[idx] = v; } } @@ -94,6 +104,8 @@ impl GrayscaleState { } impl Renderer for GrayscaleState { + type Value = f64; + fn register(trace: &Trace, func: &str) -> Option { Self::new(trace, func) } @@ -140,6 +152,10 @@ impl Renderer for GrayscaleState { } out } + + fn to_values(&self) -> Vec { + self.values.clone() + } } // ── RGB rendering ───────────────────────────────────────────────────────────── @@ -153,6 +169,7 @@ pub struct RgbState { /// Latest normalized intensity per (pixel, channel), row-major with channel as the minor axis. /// Length is `width * height * channels`. Unwritten cells stay 0. framebuffer: Vec, + values: Vec, applied_k: usize, } @@ -163,11 +180,14 @@ impl RgbState { let min_v = stats.min_value.unwrap_or(0.0); let max_v = stats.max_value.unwrap_or(255.0); let framebuffer = vec![0u8; geom.width * geom.height * geom.channels]; + let values = vec![0f64; geom.width * geom.height * geom.channels]; + Some(Self { geom, min_v, max_v, framebuffer, + values, applied_k: 0, }) } @@ -202,6 +222,7 @@ impl RgbState { } let idx = (y as usize * width + x as usize) * channels + c as usize; self.framebuffer[idx] = self.normalize(v); + self.values[idx] = v; } } @@ -212,6 +233,8 @@ impl RgbState { } impl Renderer for RgbState { + type Value = f64; + fn register(trace: &Trace, func: &str) -> Option { Self::new(trace, func) } @@ -256,6 +279,10 @@ impl Renderer for RgbState { } out } + + fn to_values(&self) -> Vec { + self.values.clone() + } } // ── Store frequency rendering ───────────────────────────────────────────────── @@ -265,16 +292,16 @@ impl Renderer for RgbState { /// is used for normalization so the color scale is stable across the entire scrub range. pub struct StoreFrequencyState { geom: FuncGeometry, - counts: Vec, - local_max_store_count: i32, - global_max_store_count: i32, + counts: Vec, + local_max_store_count: u32, + global_max_store_count: u32, applied_k: usize, } impl StoreFrequencyState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; - let counts = vec![0i32; geom.width * geom.height]; + let counts = vec![0u32; geom.width * geom.height]; let local_max_store_count = trace.funcs.get(func).map_or(0, |s| s.max_store_count); let global_max_store_count = trace @@ -313,6 +340,8 @@ impl StoreFrequencyState { } impl Renderer for StoreFrequencyState { + type Value = u32; + fn register(trace: &Trace, func: &str) -> Option { Self::new(trace, func) } @@ -364,6 +393,10 @@ impl Renderer for StoreFrequencyState { } out } + + fn to_values(&self) -> Vec { + self.counts.clone() + } } impl StoreFrequencyState { @@ -390,16 +423,16 @@ impl StoreFrequencyState { /// load count is used for normalization so the color scale is stable across the entire scrub range. pub struct LoadFrequencyState { geom: FuncGeometry, - counts: Vec, - local_max_load_count: i32, - global_max_load_count: i32, + counts: Vec, + local_max_load_count: u32, + global_max_load_count: u32, applied_k: usize, } impl LoadFrequencyState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; - let counts = vec![0i32; geom.width * geom.height]; + let counts = vec![0u32; geom.width * geom.height]; let local_max_load_count = trace.funcs.get(func).map_or(0, |s| s.max_load_count); let global_max_load_count = trace @@ -438,6 +471,8 @@ impl LoadFrequencyState { } impl Renderer for LoadFrequencyState { + type Value = u32; + fn register(trace: &Trace, func: &str) -> Option { Self::new(trace, func) } @@ -488,6 +523,10 @@ impl Renderer for LoadFrequencyState { } out } + + fn to_values(&self) -> Vec { + self.counts.clone() + } } impl LoadFrequencyState { @@ -521,9 +560,9 @@ pub struct RedundantState { /// `None` = no store has landed here yet. last_values: Vec>, /// Redundant-store count per spatial pixel, indexed by `y * width + x`. - redundant_counts: Vec, - local_max_redundant_count: i32, - global_max_redundant_count: i32, + redundant_counts: Vec, + local_max_redundant_count: u32, + global_max_redundant_count: u32, applied_k: usize, } @@ -533,8 +572,7 @@ impl RedundantState { let geom = trace.func_geometry(func)?; let n_pixels = geom.width * geom.height; - let local_max_redundant_count = - trace.funcs.get(func).map_or(0, |s| s.max_redundant_count); + let local_max_redundant_count = trace.funcs.get(func).map_or(0, |s| s.max_redundant_count); let global_max_redundant_count = trace .funcs .values() @@ -545,7 +583,7 @@ impl RedundantState { Some(Self { geom, last_values: vec![None; n_pixels * geom.channels], - redundant_counts: vec![0i32; n_pixels], + redundant_counts: vec![0u32; n_pixels], local_max_redundant_count, global_max_redundant_count, applied_k: 0, @@ -601,6 +639,8 @@ impl RedundantState { } impl Renderer for RedundantState { + type Value = u32; + fn register(trace: &Trace, func: &str) -> Option { Self::new(trace, func) } @@ -655,6 +695,10 @@ impl Renderer for RedundantState { } out } + + fn to_values(&self) -> Vec { + self.redundant_counts.clone() + } } impl RedundantState { @@ -698,13 +742,13 @@ pub struct ReuseDistanceState { /// For inputs: global index of the first load (`usize::MAX` = none yet). anchor_at: Vec, /// Maximum observed reuse distance per spatial pixel, indexed by `y * width + x`. - max_reuse_distance: Vec, + max_reuse_distance: Vec, /// This Func's own maximum reuse distance, used to normalize the color scale against just /// this Func's range. - local_max_reuse_distance: i64, + local_max_reuse_distance: u64, /// Trace-wide maximum reuse distance, used to normalize the color scale consistently across /// all Funcs regardless of which one is being viewed. - global_max_reuse_distance: i64, + global_max_reuse_distance: u64, /// Number of this Func's store events processed. applied_store_k: usize, /// Number of this Func's load events processed. @@ -733,7 +777,7 @@ impl ReuseDistanceState { geom, is_input, anchor_at: vec![usize::MAX; n_cells], - max_reuse_distance: vec![0i64; geom.width * geom.height], + max_reuse_distance: vec![0u64; geom.width * geom.height], local_max_reuse_distance, global_max_reuse_distance, applied_store_k: 0, @@ -855,13 +899,13 @@ impl ReuseDistanceState { if self.anchor_at[val_idx] == usize::MAX { self.anchor_at[val_idx] = global_idx; } else { - let dist = (global_idx - self.anchor_at[val_idx]) as i64; + let dist = (global_idx - self.anchor_at[val_idx]) as u64; if dist > self.max_reuse_distance[pixel_idx] { self.max_reuse_distance[pixel_idx] = dist; } } } else if self.anchor_at[val_idx] != usize::MAX { - let dist = (global_idx - self.anchor_at[val_idx]) as i64; + let dist = (global_idx - self.anchor_at[val_idx]) as u64; if dist > self.max_reuse_distance[pixel_idx] { self.max_reuse_distance[pixel_idx] = dist; } @@ -930,4 +974,10 @@ impl ReuseDistanceState { } hist } + + /// Returns the per-pixel maximum reuse distance at the current seek position, in row-major + /// `(y * width + x)` order. + pub fn to_values(&self) -> Vec { + self.max_reuse_distance.clone() + } } diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 9ab8669eb358..3ee734d24d43 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -170,17 +170,17 @@ pub struct FuncStats { pub min_value: Option, pub max_value: Option, /// Maximum number of stores observed at any array / tensor coordinate for this Func. - pub max_store_count: i32, + pub max_store_count: u32, /// Maximum number of loads observed at any array / tensor coordinate for this Func. - pub max_load_count: i32, + pub max_load_count: u32, /// Maximum number of redundant stores observed at any array / tensor coordinate for this Func. /// A store is considered redundant when the incoming value bit-matches the previously stored /// value at that location. - pub max_redundant_count: i32, + pub max_redundant_count: u32, /// Maximum store-to-load distance observed across all array / tensor coordinates for this Func. /// Measured as thedifference in global packet indices between a store and the next load from /// the same coordination. 0 when no store→load pair was observed. - pub max_reuse_distance: i64, + pub max_reuse_distance: u64, } impl Default for FuncStats { @@ -208,10 +208,10 @@ pub struct FuncGeometry { pub min_x: i32, pub min_y: i32, pub min_c: i32, - pub max_store_count: i32, - pub max_load_count: i32, - pub max_redundant_count: i32, - pub max_reuse_distance: i64, + pub max_store_count: u32, + pub max_load_count: u32, + pub max_redundant_count: u32, + pub max_reuse_distance: u64, } // ── Complete trace ──────────────────────────────────────────────────────────── @@ -699,7 +699,7 @@ impl Trace { for (func_name, indices) in &store_indices_by_func { let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { - let mut counts = vec![0i32; w * h]; + let mut counts = vec![0u32; w * h]; for &idx in indices { let pkt = &packets[idx]; for_each_lane_pixel( @@ -723,7 +723,7 @@ impl Trace { for (func_name, indices) in &load_indices_by_func { let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { - let mut counts = vec![0i32; w * h]; + let mut counts = vec![0u32; w * h]; for &idx in indices { let pkt = &packets[idx]; for_each_lane_pixel( @@ -768,7 +768,7 @@ impl Trace { // None = no store has landed here yet; Some(bits) = last stored value as u64 bits. let mut last_values = vec![None::; w * h * channels]; - let mut redundant_counts = vec![0i32; w * h]; + let mut redundant_counts = vec![0u32; w * h]; let mut si = 0; let mut li = 0; @@ -856,7 +856,7 @@ impl Trace { // usize::MAX = no store has landed at this (x, y, channel) yet. let mut last_store_at = vec![usize::MAX; w * h * channels]; - let mut max_reuse_distances = vec![0i64; w * h]; + let mut max_reuse_distances = vec![0u64; w * h]; let mut si = 0; let mut li = 0; @@ -892,7 +892,7 @@ impl Trace { Some((min_c, channels)), |_lane, pixel_idx, val_idx| { if last_store_at[val_idx] != usize::MAX { - let dist = (global_idx - last_store_at[val_idx]) as i64; + let dist = (global_idx - last_store_at[val_idx]) as u64; if dist > max_reuse_distances[pixel_idx] { max_reuse_distances[pixel_idx] = dist; } @@ -929,7 +929,7 @@ impl Trace { // usize::MAX = first load hasn't occurred at this (x, y, channel) yet. let mut first_load_at = vec![usize::MAX; w * h * channels]; - let mut max_reuse_distances = vec![0i64; w * h]; + let mut max_reuse_distances = vec![0u64; w * h]; for &global_idx in load_indices { let pkt = &packets[global_idx]; @@ -944,7 +944,7 @@ impl Trace { if first_load_at[val_idx] == usize::MAX { first_load_at[val_idx] = global_idx; } else { - let dist = (global_idx - first_load_at[val_idx]) as i64; + let dist = (global_idx - first_load_at[val_idx]) as u64; if dist > max_reuse_distances[pixel_idx] { max_reuse_distances[pixel_idx] = dist; } From 46acffeaad539f72e76de33e385c8b5fdebbc718 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 14 Jul 2026 15:38:04 -0700 Subject: [PATCH 25/67] Add docs on building Halidoscope, using the GUI and CLI, and developing Halidoscope locally. --- apps/halidoscope/README.md | 109 +++++++++++++++++---- apps/halidoscope/package.json | 2 +- apps/halidoscope/pnpm-lock.yaml | 98 +++++++++--------- apps/halidoscope/src-tauri/src/lib.rs | 19 ++-- apps/halidoscope/src-tauri/tauri.conf.json | 4 +- 5 files changed, 152 insertions(+), 80 deletions(-) diff --git a/apps/halidoscope/README.md b/apps/halidoscope/README.md index 160f3832ff70..a7548d07e655 100644 --- a/apps/halidoscope/README.md +++ b/apps/halidoscope/README.md @@ -1,48 +1,115 @@ # Halidoscope -(Another) Interactive trace visualizer for Halide. +An interactive GUI and CLI for working with Halide traces. ## Prerequisites -You'll need a few prerequisites (in addition to the usual Halide development -setup) to get everything working. +You'll need a few prerequisites to get everything working. -1. A [Rust](https://rust-lang.org/learn/get-started/) installation. -2. A [Node.js](https://nodejs.org/en/download) installation. -3. [PNPM](https://pnpm.io/), a space-efficient package manager for the +1. Tauri's + [system dependencies](https://v2.tauri.app/start/prerequisites/#system-dependencies) + for your OS. + - Note that you only need dependencies for desktop targets. +2. A [Rust](https://rust-lang.org/learn/get-started/) installation. +3. A [Node.js](https://nodejs.org/en/download) installation. +4. [PNPM](https://pnpm.io/), a space-efficient package manager for the JavaScript ecosystem. -> You can likely get away with using NPM directly, but `npm install` will not -> respect the version ranges in `pnpm-lock.yaml`. +## Building Halidoscope -## Development +To get a production build locally, run the following two commands: -### Backend +```sh +pnpm install +pnpm tauri build +``` + +This will write the Halidoscope executable to +`/src-tauri/target/release/halidoscope`. You can, of course, symlink +this executable to any directory on your `PATH`. On Unix systems: ```sh -uv sync --no-install-project halide +ln -sf /path/to/thisDir/src-tauri/target/release/halidoscope /some/dir/on/your/path/halidoscope ``` -### Frontend +## Using Halidoscope + +### Running the GUI + +To run the GUI, pass a Halide trace binary file to `halidoscope` via the +`--trace` flag. ```sh -pnpm install +halidoscope --trace +``` + +This will load the specified trace file and start up the GUI. + +### Using the CLI + +`halidoscope` also exposes a CLI for gathering information about your Halide +pipeline. + +#### `list` + +List the `Func`s in a trace, along with their dimensionality. + +```sh +halidoscope list --trace [--json] ``` -## Starting Things Up +- `-t, --trace ` (required): Path to the `.hltrace` file to analyze. +- `--json`: Print output as JSON instead of a table. -1. Run the backend locally. +#### `stats` + +Print statistics (minimum/maximum coordinates, minimum/maximum value, maximum +store/load counts, and thread count) for one or all `Func`s in a trace. ```sh -cd backend -uv run dev +halidoscope stats --trace [--func ] [--json] ``` -2. Run the frontend, pointing it at a Halide trace. +- `-t, --trace ` (required): Path to the `.hltrace` file to analyze. +- `-f, --func `: Name of the Func to print statistics for. If omitted, + prints statistics for all Funcs. +- `--json`: Print output as JSON instead of a table. + +#### `dot` + +Generate a [Graphviz DOT](https://graphviz.org/doc/info/lang.html) +representation of the pipeline's dataflow graph. ```sh -cd frontend -pnpm tauri dev -- -- --trace +halidoscope dot --trace [destination] ``` -This should launch Halidoscope in development mode. +- `-t, --trace ` (required): Path to the `.hltrace` file to analyze. +- `destination` (optional): Path to write the DOT file. Must end in `.txt`, + `.gv`, or `.dot`. If omitted, prints the DOT source to stdout. + +#### `snapshot` + +Snapshot a `Func`'s values at a given packet index for a given rendering mode, +writing the underlying data to a JSON file. + +```sh +halidoscope snapshot --trace --func [--packet-index ] [--mode ] +``` + +- `-t, --trace ` (required): Path to the `.hltrace` file to snapshot. +- `-f, --func ` (required): Name of the Func to snapshot. +- `-i, --packet-index `: Global packet index to snapshot. Defaults to `0`. +- `-m, --mode `: Rendering mode. One of `grayscale` (default), `rgb`, + `store-frequency`, `load-frequency`, `redundant-stores`, or `reuse-distance`. +- `destination` (required): Path to write the output snapshot. Must end in + `.json`. + +## Developing Halidoscope + +To develop Halidoscope locally, run the following two commands: + +```sh +pnpm install +pnpm tauri dev -- -- --trace +``` diff --git a/apps/halidoscope/package.json b/apps/halidoscope/package.json index 29185679ec51..b8d33747f348 100644 --- a/apps/halidoscope/package.json +++ b/apps/halidoscope/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@tauri-apps/cli": "^2", + "@tauri-apps/cli": "^2.11.4", "@types/d3": "^7.4.3", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", diff --git a/apps/halidoscope/pnpm-lock.yaml b/apps/halidoscope/pnpm-lock.yaml index 0bc01a4b1dda..b295218cec07 100644 --- a/apps/halidoscope/pnpm-lock.yaml +++ b/apps/halidoscope/pnpm-lock.yaml @@ -55,8 +55,8 @@ importers: specifier: ^10.0.1 version: 10.0.1(eslint@10.4.1(jiti@2.7.0)) '@tauri-apps/cli': - specifier: ^2 - version: 2.11.2 + specifier: ^2.11.4 + version: 2.11.4 '@types/d3': specifier: ^7.4.3 version: 7.4.3 @@ -1182,79 +1182,79 @@ packages: '@tauri-apps/api@2.11.0': resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} - '@tauri-apps/cli-darwin-arm64@2.11.2': - resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} + '@tauri-apps/cli-darwin-arm64@2.11.4': + resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.11.2': - resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} + '@tauri-apps/cli-darwin-x64@2.11.4': + resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': - resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.11.2': - resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-arm64-musl@2.11.2': - resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} + '@tauri-apps/cli-linux-arm64-musl@2.11.4': + resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': - resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-gnu@2.11.2': - resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} + '@tauri-apps/cli-linux-x64-gnu@2.11.4': + resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-musl@2.11.2': - resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} + '@tauri-apps/cli-linux-x64-musl@2.11.4': + resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@tauri-apps/cli-win32-arm64-msvc@2.11.2': - resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.11.2': - resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.11.2': - resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} + '@tauri-apps/cli-win32-x64-msvc@2.11.4': + resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.11.2': - resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} + '@tauri-apps/cli@2.11.4': + resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==} engines: {node: '>= 10'} hasBin: true @@ -3450,52 +3450,52 @@ snapshots: '@tauri-apps/api@2.11.0': {} - '@tauri-apps/cli-darwin-arm64@2.11.2': + '@tauri-apps/cli-darwin-arm64@2.11.4': optional: true - '@tauri-apps/cli-darwin-x64@2.11.2': + '@tauri-apps/cli-darwin-x64@2.11.4': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + '@tauri-apps/cli-linux-arm64-gnu@2.11.4': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.11.2': + '@tauri-apps/cli-linux-arm64-musl@2.11.4': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.11.2': + '@tauri-apps/cli-linux-x64-gnu@2.11.4': optional: true - '@tauri-apps/cli-linux-x64-musl@2.11.2': + '@tauri-apps/cli-linux-x64-musl@2.11.4': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + '@tauri-apps/cli-win32-arm64-msvc@2.11.4': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + '@tauri-apps/cli-win32-ia32-msvc@2.11.4': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.11.2': + '@tauri-apps/cli-win32-x64-msvc@2.11.4': optional: true - '@tauri-apps/cli@2.11.2': + '@tauri-apps/cli@2.11.4': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.11.2 - '@tauri-apps/cli-darwin-x64': 2.11.2 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.2 - '@tauri-apps/cli-linux-arm64-gnu': 2.11.2 - '@tauri-apps/cli-linux-arm64-musl': 2.11.2 - '@tauri-apps/cli-linux-riscv64-gnu': 2.11.2 - '@tauri-apps/cli-linux-x64-gnu': 2.11.2 - '@tauri-apps/cli-linux-x64-musl': 2.11.2 - '@tauri-apps/cli-win32-arm64-msvc': 2.11.2 - '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 - '@tauri-apps/cli-win32-x64-msvc': 2.11.2 + '@tauri-apps/cli-darwin-arm64': 2.11.4 + '@tauri-apps/cli-darwin-x64': 2.11.4 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.4 + '@tauri-apps/cli-linux-arm64-musl': 2.11.4 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-gnu': 2.11.4 + '@tauri-apps/cli-linux-x64-musl': 2.11.4 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.4 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 + '@tauri-apps/cli-win32-x64-msvc': 2.11.4 '@tauri-apps/plugin-cli@2.4.1': dependencies: diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index bb568cda51f0..71b0f276eb7b 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -10,13 +10,18 @@ pub mod trace; #[tauri::command] fn get_cwd() -> Result { - std::env::current_dir() - .map_err(|e| e.to_string()) - .and_then(|p| { - p.parent() - .map(|parent| parent.to_string_lossy().into_owned()) - .ok_or_else(|| "no parent directory".to_string()) - }) + let cwd = std::env::current_dir().map_err(|e| e.to_string())?; + + // In dev, `cargo run` is invoked with its cwd set to `src-tauri`, so walk + // up one level to match the directory the user launched `tauri dev` from. + // The bundled binary is launched directly, so its cwd needs no adjustment. + if cfg!(debug_assertions) { + cwd.parent() + .map(|parent| parent.to_string_lossy().into_owned()) + .ok_or_else(|| "no parent directory".to_string()) + } else { + Ok(cwd.to_string_lossy().into_owned()) + } } #[cfg_attr(mobile, tauri::mobile_entry_point)] diff --git a/apps/halidoscope/src-tauri/tauri.conf.json b/apps/halidoscope/src-tauri/tauri.conf.json index 2f1266e49963..0cb5107f9981 100644 --- a/apps/halidoscope/src-tauri/tauri.conf.json +++ b/apps/halidoscope/src-tauri/tauri.conf.json @@ -23,8 +23,8 @@ } }, "bundle": { - "active": true, - "targets": "all", + "active": false, + "targets": [], "icon": [ "icons/32x32.png", "icons/128x128.png", From 608d94d56decb14679005a7a5798cbebff259122 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Fri, 17 Jul 2026 15:46:00 -0700 Subject: [PATCH 26/67] Add support for NaN and inf rendering. --- apps/halidoscope/src-tauri/src/commands.rs | 81 +- apps/halidoscope/src-tauri/src/lib.rs | 4 +- apps/halidoscope/src-tauri/src/render.rs | 712 +++++++++++------- apps/halidoscope/src-tauri/src/trace.rs | 47 +- apps/halidoscope/src/App.css | 8 + .../src/components/canvas/FuncNode.tsx | 106 ++- .../src/components/controls/ControlTabs.tsx | 10 + .../src/components/controls/DebugPanel.tsx | 21 + .../components/controls/inf/InfControls.tsx | 76 ++ .../components/controls/nan/NaNControls.tsx | 76 ++ .../components/controls/render/RenderMode.tsx | 16 +- apps/halidoscope/src/state/inf.ts | 8 + apps/halidoscope/src/state/nan.ts | 8 + apps/halidoscope/src/types/index.ts | 4 +- apps/halidoscope/src/utils/api.ts | 44 +- apps/halidoscope/src/utils/constants.ts | 4 + 16 files changed, 888 insertions(+), 337 deletions(-) create mode 100644 apps/halidoscope/src/components/controls/DebugPanel.tsx create mode 100644 apps/halidoscope/src/components/controls/inf/InfControls.tsx create mode 100644 apps/halidoscope/src/components/controls/nan/NaNControls.tsx create mode 100644 apps/halidoscope/src/state/inf.ts create mode 100644 apps/halidoscope/src/state/nan.ts diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 3a5b722d2504..b121cff36843 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -10,8 +10,8 @@ use tauri::ipc::Response; use tauri::State; use crate::render::{ - GrayscaleState, LoadFrequencyState, NormalizationMode, RedundantState, Renderer, - ReuseDistanceState, RgbState, StoreFrequencyState, + GrayscaleState, InfState, LoadFrequencyState, NaNState, NormalizationMode, RedundantState, + Renderer, ReuseDistanceState, RgbState, StoreFrequencyState, }; use crate::trace::Trace; @@ -42,7 +42,7 @@ pub struct FuncMeta { pub max_value: Option, pub max_store_count: u32, pub max_load_count: u32, - pub max_redundant_count: u32, + pub max_redundant_store_count: u32, pub max_reuse_distance: u64, pub buffer_liveness: IndexRange, pub produce_ranges: Vec, @@ -92,7 +92,7 @@ impl TraceMeta { max_value: stats.max_value, max_store_count: stats.max_store_count, max_load_count: stats.max_load_count, - max_redundant_count: stats.max_redundant_count, + max_redundant_store_count: stats.max_redundant_store_count, max_reuse_distance: stats.max_reuse_distance, buffer_liveness: IndexRange::from_tuple( trace @@ -150,6 +150,8 @@ struct Loaded { load_frequency_renderers: HashMap, redundant_renderers: HashMap, reuse_distance_renderers: HashMap, + nan_renderers: HashMap, + inf_renderers: HashMap, } /// App-wide state managed by Tauri. A single trace is loaded at a time; opening a new one replaces @@ -188,6 +190,8 @@ pub fn open_trace(path: String, state: State) -> Result, +) -> Result { + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + let loaded = guard.as_mut().ok_or("no trace loaded")?; + let Loaded { + trace, + nan_renderers, + .. + } = loaded; + + if !nan_renderers.contains_key(&func) { + let rs = NaNState::new(trace, &func) + .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; + nan_renderers.insert(func.clone(), rs); + } + let renderer = nan_renderers.get_mut(&func).expect("just inserted"); + + let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); + let k = store_indices.partition_point(|&p| p <= global_index as usize); + renderer.seek(trace, store_indices, k); + + Ok(Response::new(renderer.to_rgba(normalization_mode))) +} + +#[tauri::command] +pub fn render_inf( + func: String, + global_index: u32, + normalization_mode: NormalizationMode, + state: State, +) -> Result { + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + let loaded = guard.as_mut().ok_or("no trace loaded")?; + let Loaded { + trace, + inf_renderers, + .. + } = loaded; + + if !inf_renderers.contains_key(&func) { + let rs = InfState::new(trace, &func) + .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; + inf_renderers.insert(func.clone(), rs); + } + let renderer = inf_renderers.get_mut(&func).expect("just inserted"); + + let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); + let k = store_indices.partition_point(|&p| p <= global_index as usize); + renderer.seek(trace, store_indices, k); + + Ok(Response::new(renderer.to_rgba(normalization_mode))) +} diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index 71b0f276eb7b..b001d0bd35ea 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -57,7 +57,9 @@ pub fn run() { commands::render_store_frequency, commands::render_load_frequency, commands::render_redundant_stores, - commands::render_reuse_distance + commands::render_reuse_distance, + commands::render_nan, + commands::render_inf, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index 674bec7e5452..16330ffa2d67 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -1,13 +1,7 @@ -//! Framebuffer rendering for Halidoscope. -//! -//! Each rendering pathway has its own state type that accumulates pixel data from trace events -//! and emits RGBA8 for `putImageData`. States are decoupled from `Trace`: event index slices are -//! passed into `seek` so states can live in Tauri-managed storage alongside the parsed trace. - use ::colorous; use serde::Deserialize; -use crate::trace::{pixel_xy, FuncGeometry, Trace, TracePacket}; +use crate::trace::{for_each_lane_pixel, pixel_xy, FuncGeometry, Trace, TracePacket}; #[derive(Deserialize, Clone, Copy)] pub enum NormalizationMode { @@ -17,10 +11,8 @@ pub enum NormalizationMode { PerFunc, } -// A trait that all 2D rendering states implement. +// A trait that all 2D Canvas renderers implement. pub trait Renderer: Sized { - /// The primitive type this state's `values` are stored and returned as (e.g. `f64` for - /// intensity-accumulating states, `u32`/`i32` for count-accumulating states). type Value; fn register(trace: &Trace, func: &str) -> Option; @@ -29,16 +21,12 @@ pub trait Renderer: Sized { fn to_values(&self) -> Vec; } -// ── Grayscale rendering ─────────────────────────────────────────────────────── +// ── Grayscale rendering ────────────────────────────────────────────────────────────────────────── -/// Accumulated pixel state for a single Func. Channel 0 is normalized to [0, 255] and replicated -/// across R/G/B in `to_rgba`. Forward seeks apply only the delta; backward seeks clear and replay. pub struct GrayscaleState { geom: FuncGeometry, min_v: f64, max_v: f64, - /// Latest normalized intensity per (pixel, channel), row-major with channel as the minor axis. - /// Length is `width * height * channels`. Unwritten cells stay 0. framebuffer: Vec, values: Vec, applied_k: usize, @@ -64,8 +52,6 @@ impl GrayscaleState { } fn apply_store(&mut self, pkt: &TracePacket) { - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; let FuncGeometry { width, height, @@ -75,26 +61,23 @@ impl GrayscaleState { min_c, .. } = self.geom; - for lane in 0..n_lanes { - let Some(v) = pkt.decoded_value(lane) else { - continue; - }; - let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); - if x < 0 || y < 0 || x as usize >= width || y as usize >= height { - continue; - } - let c = if dims_per_lane >= 3 { - pkt.coordinates[2 * n_lanes + lane] - min_c - } else { - 0 - }; - if c < 0 || c as usize >= channels { - continue; - } - let idx = (y as usize * width + x as usize) * channels + c as usize; - self.framebuffer[idx] = self.normalize(v); - self.values[idx] = v; - } + + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + Some((min_c, channels)), + |lane, _pixel_idx, val_idx, _x, _y| { + let Some(v) = pkt.decoded_value(lane) else { + return; + }; + + self.framebuffer[val_idx] = self.normalize(v); + self.values[val_idx] = v; + }, + ); } #[inline] @@ -122,7 +105,6 @@ impl Renderer for GrayscaleState { self.applied_k = target_k; } - /// Produces a `width * height * 4` RGBA8 buffer. Channel 0 is replicated across R/G/B. fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, @@ -158,16 +140,12 @@ impl Renderer for GrayscaleState { } } -// ── RGB rendering ───────────────────────────────────────────────────────────── +// ── RGB rendering ──────────────────────────────────────────────────────────────────────────────── -/// Same accumulation logic as `GrayscaleState`, but `to_rgba` maps planes 0/1/2 directly to -/// R/G/B. Missing planes default to 0. Alpha is always opaque. pub struct RgbState { geom: FuncGeometry, min_v: f64, max_v: f64, - /// Latest normalized intensity per (pixel, channel), row-major with channel as the minor axis. - /// Length is `width * height * channels`. Unwritten cells stay 0. framebuffer: Vec, values: Vec, applied_k: usize, @@ -193,8 +171,6 @@ impl RgbState { } fn apply_store(&mut self, pkt: &TracePacket) { - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; let FuncGeometry { width, height, @@ -204,26 +180,23 @@ impl RgbState { min_c, .. } = self.geom; - for lane in 0..n_lanes { - let Some(v) = pkt.decoded_value(lane) else { - continue; - }; - let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); - if x < 0 || y < 0 || x as usize >= width || y as usize >= height { - continue; - } - let c = if dims_per_lane >= 3 { - pkt.coordinates[2 * n_lanes + lane] - min_c - } else { - 0 - }; - if c < 0 || c as usize >= channels { - continue; - } - let idx = (y as usize * width + x as usize) * channels + c as usize; - self.framebuffer[idx] = self.normalize(v); - self.values[idx] = v; - } + + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + Some((min_c, channels)), + |lane, _pixel_idx, val_idx, _x, _y| { + let Some(v) = pkt.decoded_value(lane) else { + return; + }; + + self.framebuffer[val_idx] = self.normalize(v); + self.values[val_idx] = v; + }, + ); } #[inline] @@ -251,8 +224,6 @@ impl Renderer for RgbState { self.applied_k = target_k; } - /// Produces a `width * height * 4` RGBA8 buffer. Planes 0/1/2 map to R/G/B; - /// missing planes are 0. Alpha is always opaque. fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, @@ -285,11 +256,8 @@ impl Renderer for RgbState { } } -// ── Store frequency rendering ───────────────────────────────────────────────── +// ── Store frequency rendering ──────────────────────────────────────────────────────────────────── -/// Accumulated per-pixel store counts for one Func, seekable along the global timeline. Forward -/// seeks apply only the new events; backward seeks clear and replay. The global max store count -/// is used for normalization so the color scale is stable across the entire scrub range. pub struct StoreFrequencyState { geom: FuncGeometry, counts: Vec, @@ -332,11 +300,26 @@ impl StoreFrequencyState { let dims_per_lane = pkt.coordinates.len() / n_lanes; for l in 0..n_lanes { let (x, y) = pixel_xy(pkt, l, n_lanes, dims_per_lane, min_x, min_y); + if x >= 0 && y >= 0 && (x as usize) < width && (y as usize) < height { self.counts[y as usize * width + x as usize] += 1; } } } + + pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + let max = match normalization_mode { + NormalizationMode::AcrossFuncs => self.global_max_store_count, + NormalizationMode::PerFunc => self.local_max_store_count, + }; + let mut hist = vec![0u32; max as usize + 1]; + + for &c in &self.counts { + hist[c.clamp(0, max) as usize] += 1; + } + + hist + } } impl Renderer for StoreFrequencyState { @@ -358,9 +341,6 @@ impl Renderer for StoreFrequencyState { self.applied_k = target_k; } - /// Produces a `width × height × 4` RGBA8 buffer with the Inferno colormap applied. Counts - /// are normalized against the global full-trace maximum so intensities are comparable across - /// all Funcs. fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; @@ -371,7 +351,7 @@ impl Renderer for StoreFrequencyState { let scale = match (normalization_mode, self.global_max_store_count) { (NormalizationMode::AcrossFuncs, 0) => 0.0, - (NormalizationMode::AcrossFuncs, max) => 255.0 / max as f64, + (NormalizationMode::AcrossFuncs, global_max) => 255.0 / global_max as f64, (NormalizationMode::PerFunc, _) => { let local_max = *&self.local_max_store_count; if local_max > 0 { @@ -399,28 +379,8 @@ impl Renderer for StoreFrequencyState { } } -impl StoreFrequencyState { - /// Produces a frequency histogram of live per-pixel store counts at the current seek - /// position. `hist[k]` is the number of pixel locations currently stored exactly `k` times, - /// for `k` in `0..=chosen_max`, where `chosen_max` is this Func's own max (`PerFunc`) or the - /// trace-wide max (`AcrossFuncs`) — matching whichever max drives `to_rgba`'s color scale. - pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { - let chosen_max = match normalization_mode { - NormalizationMode::AcrossFuncs => self.global_max_store_count, - NormalizationMode::PerFunc => self.local_max_store_count, - }; - let mut hist = vec![0u32; chosen_max as usize + 1]; - for &c in &self.counts { - hist[c.clamp(0, chosen_max) as usize] += 1; - } - hist - } -} - -// ── Load frequency rendering ────────────────────────────────────────────────── +// ── Load frequency rendering ───────────────────────────────────────────────────────────────────── -/// Mirrors `StoreFrequencyState` but tracks load events instead of store events. The global max -/// load count is used for normalization so the color scale is stable across the entire scrub range. pub struct LoadFrequencyState { geom: FuncGeometry, counts: Vec, @@ -459,14 +419,32 @@ impl LoadFrequencyState { min_y, .. } = self.geom; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for l in 0..n_lanes { - let (x, y) = pixel_xy(pkt, l, n_lanes, dims_per_lane, min_x, min_y); - if x >= 0 && y >= 0 && (x as usize) < width && (y as usize) < height { - self.counts[y as usize * width + x as usize] += 1; - } + + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + None, + |_lane, pixel_idx, _val_idx, _x, _y| { + self.counts[pixel_idx] += 1; + }, + ); + } + + pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + let max = match normalization_mode { + NormalizationMode::AcrossFuncs => self.global_max_load_count, + NormalizationMode::PerFunc => self.local_max_load_count, + }; + let mut hist = vec![0u32; max as usize + 1]; + + for &c in &self.counts { + hist[c.clamp(0, max) as usize] += 1; } + + hist } } @@ -489,9 +467,6 @@ impl Renderer for LoadFrequencyState { self.applied_k = target_k; } - /// Produces a `width × height × 4` RGBA8 buffer with the Inferno colormap applied. Counts - /// are normalized against the global full-trace maximum so intensities are comparable across - /// all Funcs. fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; let gradient = colorous::INFERNO; @@ -502,7 +477,7 @@ impl Renderer for LoadFrequencyState { let scale = match (normalization_mode, self.global_max_load_count) { (NormalizationMode::AcrossFuncs, 0) => 0.0, - (NormalizationMode::AcrossFuncs, max) => 255.0 / max as f64, + (NormalizationMode::AcrossFuncs, global_max) => 255.0 / global_max as f64, (NormalizationMode::PerFunc, _) => { if self.local_max_load_count > 0 { 255.0 / self.local_max_load_count as f64 @@ -529,40 +504,14 @@ impl Renderer for LoadFrequencyState { } } -impl LoadFrequencyState { - /// Produces a frequency histogram of live per-pixel load counts at the current seek - /// position. `hist[k]` is the number of pixel locations currently loaded exactly `k` times, - /// for `k` in `0..=chosen_max`, where `chosen_max` is this Func's own max (`PerFunc`) or the - /// trace-wide max (`AcrossFuncs`) — matching whichever max drives `to_rgba`'s color scale. - pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { - let chosen_max = match normalization_mode { - NormalizationMode::AcrossFuncs => self.global_max_load_count, - NormalizationMode::PerFunc => self.local_max_load_count, - }; - let mut hist = vec![0u32; chosen_max as usize + 1]; - for &c in &self.counts { - hist[c.clamp(0, chosen_max) as usize] += 1; - } - hist - } -} - -// ── Redundant computation rendering ────────────────────────────────────────── +// ── Redundant store rendering ──────────────────────────────────────────────────────────────────── -/// Accumulated per-pixel redundant-store counts for one Func. A store to pixel (x, y, c) is -/// redundant when it writes the same bit-pattern that was last stored there. The full-trace max -/// redundant count (pre-computed at parse time) is used for normalization so the color scale is -/// stable across the entire scrub range. pub struct RedundantState { geom: FuncGeometry, - /// Last value stored per (pixel × channel), flat row-major: - /// `last_values[(y * width + x) * channels + c]`. - /// `None` = no store has landed here yet. last_values: Vec>, - /// Redundant-store count per spatial pixel, indexed by `y * width + x`. - redundant_counts: Vec, - local_max_redundant_count: u32, - global_max_redundant_count: u32, + redundant_store_counts: Vec, + local_max_redundant_store_count: u32, + global_max_redundant_store_count: u32, applied_k: usize, } @@ -572,33 +521,31 @@ impl RedundantState { let geom = trace.func_geometry(func)?; let n_pixels = geom.width * geom.height; - let local_max_redundant_count = trace.funcs.get(func).map_or(0, |s| s.max_redundant_count); - let global_max_redundant_count = trace + let local_max_redundant_store_count = + trace.funcs.get(func).map(|s| s.max_redundant_store_count)?; + let global_max_redundant_store_count = trace .funcs .values() - .map(|s| s.max_redundant_count) - .max() - .unwrap_or(0); + .map(|s| s.max_redundant_store_count) + .max()?; Some(Self { geom, last_values: vec![None; n_pixels * geom.channels], - redundant_counts: vec![0u32; n_pixels], - local_max_redundant_count, - global_max_redundant_count, + redundant_store_counts: vec![0u32; n_pixels], + local_max_redundant_store_count, + global_max_redundant_store_count, applied_k: 0, }) } fn reset(&mut self) { self.last_values.iter_mut().for_each(|v| *v = None); - self.redundant_counts.iter_mut().for_each(|c| *c = 0); + self.redundant_store_counts.iter_mut().for_each(|c| *c = 0); self.applied_k = 0; } fn apply_store(&mut self, pkt: &TracePacket) { - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; let FuncGeometry { width, height, @@ -609,32 +556,39 @@ impl RedundantState { .. } = self.geom; - for lane in 0..n_lanes { - let Some(v) = pkt.decoded_value(lane) else { - continue; - }; - let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); - if x < 0 || y < 0 || x as usize >= width || y as usize >= height { - continue; - } - let c = if dims_per_lane >= 3 { - pkt.coordinates[2 * n_lanes + lane] - min_c - } else { - 0 - }; - if c < 0 || c as usize >= channels { - continue; - } - let val_idx = (y as usize * width + x as usize) * channels + c as usize; - let pixel_idx = y as usize * width + x as usize; - let v_bits = v.to_bits(); - if let Some(prev_bits) = self.last_values[val_idx] { - if prev_bits == v_bits { - self.redundant_counts[pixel_idx] += 1; + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + Some((min_c, channels)), + |lane, pixel_idx: usize, val_idx: usize, _x, _y| { + let Some(v) = pkt.decoded_value(lane) else { + return; + }; + + let v_bits = v.to_bits(); + if let Some(prev_bits) = self.last_values[val_idx] { + if prev_bits == v_bits { + self.redundant_store_counts[pixel_idx] += 1; + } } - } - self.last_values[val_idx] = Some(v_bits); + self.last_values[val_idx] = Some(v_bits); + }, + ); + } + + pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + let max = match normalization_mode { + NormalizationMode::AcrossFuncs => self.global_max_redundant_store_count, + NormalizationMode::PerFunc => self.local_max_redundant_store_count, + }; + let mut hist = vec![0u32; max as usize + 1]; + for &c in &self.redundant_store_counts { + hist[c.clamp(0, max) as usize] += 1; } + hist } } @@ -645,8 +599,6 @@ impl Renderer for RedundantState { Self::new(trace, func) } - /// Seeks to the state after the first `target_k` store events. Forward seeks apply only the - /// delta; backward seeks reset and replay from zero. fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { let target_k = target_k.min(store_indices.len()); if target_k < self.applied_k { @@ -658,9 +610,6 @@ impl Renderer for RedundantState { self.applied_k = target_k; } - /// Produces a `width × height × 4` RGBA8 buffer. Pixels with zero redundant stores are black; - /// pixels with one or more are mapped through the Inferno colormap, normalized against the - /// global full-trace maximum so intensities are comparable across all Funcs. fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; @@ -670,12 +619,12 @@ impl Renderer for RedundantState { [c.r, c.g, c.b] }); - let scale = match (normalization_mode, self.global_max_redundant_count) { + let scale = match (normalization_mode, self.global_max_redundant_store_count) { (NormalizationMode::AcrossFuncs, 0) => 0.0, - (NormalizationMode::AcrossFuncs, max) => 255.0 / max as f64, + (NormalizationMode::AcrossFuncs, global_max) => 255.0 / global_max as f64, (NormalizationMode::PerFunc, _) => { - if self.local_max_redundant_count > 0 { - 255.0 / self.local_max_redundant_count as f64 + if self.local_max_redundant_store_count > 0 { + 255.0 / self.local_max_redundant_store_count as f64 } else { 0.0 } @@ -683,7 +632,10 @@ impl Renderer for RedundantState { }; let mut out = vec![0u8; width * height * 4]; - for (chunk, &count) in out.chunks_exact_mut(4).zip(self.redundant_counts.iter()) { + for (chunk, &count) in out + .chunks_exact_mut(4) + .zip(self.redundant_store_counts.iter()) + { if count > 0 { let ti = (count as f64 * scale) as usize; let [r, g, b] = lut[ti.min(255)]; @@ -697,30 +649,11 @@ impl Renderer for RedundantState { } fn to_values(&self) -> Vec { - self.redundant_counts.clone() - } -} - -impl RedundantState { - /// Produces a frequency histogram of live per-pixel redundant-store counts at the current - /// seek position. `hist[k]` is the number of pixel locations with exactly `k` redundant - /// stores so far, for `k` in `0..=chosen_max`, where `chosen_max` is this Func's own max - /// (`PerFunc`) or the trace-wide max (`AcrossFuncs`) — matching whichever max drives - /// `to_rgba`'s color scale. - pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { - let chosen_max = match normalization_mode { - NormalizationMode::AcrossFuncs => self.global_max_redundant_count, - NormalizationMode::PerFunc => self.local_max_redundant_count, - }; - let mut hist = vec![0u32; chosen_max as usize + 1]; - for &c in &self.redundant_counts { - hist[c.clamp(0, chosen_max) as usize] += 1; - } - hist + self.redundant_store_counts.clone() } } -// ── Reuse distance rendering ────────────────────────────────────────────────── +// ── Reuse distance rendering ───────────────────────────────────────────────────────────────────── /// Per-pixel maximum reuse distance for one Func, seekable along the global timeline. /// @@ -734,30 +667,20 @@ impl RedundantState { /// Backward seeks reset and replay from zero. pub struct ReuseDistanceState { geom: FuncGeometry, - /// Whether this Func is a pipeline input (no store events in the trace). is_input: bool, - /// Per `(x, y, channel)` anchor, flat row-major: - /// `anchor_at[(y * width + x) * channels + c]`. + /// Per `(x, y, channel)` anchor, flat row-major: `anchor_at[(y * width + x) * channels + c]`. /// For intermediate Funcs: global index of the most recent store (`usize::MAX` = none yet). /// For inputs: global index of the first load (`usize::MAX` = none yet). anchor_at: Vec, /// Maximum observed reuse distance per spatial pixel, indexed by `y * width + x`. max_reuse_distance: Vec, - /// This Func's own maximum reuse distance, used to normalize the color scale against just - /// this Func's range. local_max_reuse_distance: u64, - /// Trace-wide maximum reuse distance, used to normalize the color scale consistently across - /// all Funcs regardless of which one is being viewed. global_max_reuse_distance: u64, - /// Number of this Func's store events processed. applied_store_k: usize, - /// Number of this Func's load events processed. applied_load_k: usize, } impl ReuseDistanceState { - /// Builds an empty reuse distance state for `func`, or `None` if the Func has no usable - /// geometry. pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let n_cells = geom.width * geom.height * geom.channels; @@ -765,13 +688,8 @@ impl ReuseDistanceState { .func_store_indices(func) .map_or(true, |s| s.is_empty()); - let local_max_reuse_distance = trace.funcs.get(func).map_or(0, |s| s.max_reuse_distance); - let global_max_reuse_distance = trace - .funcs - .values() - .map(|s| s.max_reuse_distance) - .max() - .unwrap_or(0); + let local_max_reuse_distance = trace.funcs.get(func).map(|s| s.max_reuse_distance)?; + let global_max_reuse_distance = trace.funcs.values().map(|s| s.max_reuse_distance).max()?; Some(Self { geom, @@ -837,6 +755,7 @@ impl ReuseDistanceState { if self.is_input { return; } + let FuncGeometry { width, height, @@ -846,24 +765,18 @@ impl ReuseDistanceState { min_c, .. } = self.geom; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for lane in 0..n_lanes { - let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); - if x < 0 || y < 0 || x as usize >= width || y as usize >= height { - continue; - } - let c = if dims_per_lane >= 3 { - pkt.coordinates[2 * n_lanes + lane] - min_c - } else { - 0 - }; - if c < 0 || c as usize >= channels { - continue; - } - let val_idx = (y as usize * width + x as usize) * channels + c as usize; - self.anchor_at[val_idx] = global_idx; - } + + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + Some((min_c, channels)), + |_lane, _pixel_idx, val_idx, _x, _y| { + self.anchor_at[val_idx] = global_idx; + }, + ); } fn apply_load(&mut self, pkt: &TracePacket, global_idx: usize) { @@ -876,46 +789,34 @@ impl ReuseDistanceState { min_c, .. } = self.geom; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for lane in 0..n_lanes { - let (x, y) = pixel_xy(pkt, lane, n_lanes, dims_per_lane, min_x, min_y); - if x < 0 || y < 0 || x as usize >= width || y as usize >= height { - continue; - } - let c = if dims_per_lane >= 3 { - pkt.coordinates[2 * n_lanes + lane] - min_c - } else { - 0 - }; - if c < 0 || c as usize >= channels { - continue; - } - let val_idx = (y as usize * width + x as usize) * channels + c as usize; - let pixel_idx = y as usize * width + x as usize; - if self.is_input { - // First load is the free memcpy; establish the anchor and record no distance. - // Subsequent loads to the same location measure from that first load. - if self.anchor_at[val_idx] == usize::MAX { - self.anchor_at[val_idx] = global_idx; - } else { - let dist = (global_idx - self.anchor_at[val_idx]) as u64; - if dist > self.max_reuse_distance[pixel_idx] { - self.max_reuse_distance[pixel_idx] = dist; + + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + Some((min_c, channels)), + |_lane, pixel_idx, val_idx, _x, _y| { + if self.is_input { + // First load is the free memcpy; establish the anchor and record no distance. + // Subsequent loads to the same location measure from that first load. + if self.anchor_at[val_idx] == usize::MAX { + self.anchor_at[val_idx] = global_idx; + } else { + let dist = (global_idx - self.anchor_at[val_idx]) as u64; + self.max_reuse_distance[pixel_idx] = + self.max_reuse_distance[pixel_idx].max(dist); } + } else if self.anchor_at[val_idx] != usize::MAX { + let dist = (global_idx - self.anchor_at[val_idx]) as u64; + self.max_reuse_distance[pixel_idx] = + self.max_reuse_distance[pixel_idx].max(dist); } - } else if self.anchor_at[val_idx] != usize::MAX { - let dist = (global_idx - self.anchor_at[val_idx]) as u64; - if dist > self.max_reuse_distance[pixel_idx] { - self.max_reuse_distance[pixel_idx] = dist; - } - } - } + }, + ); } - /// Produces a `width × height × 4` RGBA8 buffer. Pixels with no observed store→load pair are - /// black; positive distances map through the Inferno colormap normalized against the per-Func - /// full-trace maximum so the scale is stable while scrubbing. pub fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; @@ -927,7 +828,7 @@ impl ReuseDistanceState { let scale = match (normalization_mode, self.global_max_reuse_distance) { (NormalizationMode::AcrossFuncs, 0) => 0.0, - (NormalizationMode::AcrossFuncs, max) => 255.0 / max as f64, + (NormalizationMode::AcrossFuncs, global_max) => 255.0 / global_max as f64, (NormalizationMode::PerFunc, _) => { if self.local_max_reuse_distance > 0 { 255.0 / self.local_max_reuse_distance as f64 @@ -951,27 +852,22 @@ impl ReuseDistanceState { out } - /// Produces a fixed 64-bucket histogram of live per-pixel maximum reuse distances at the - /// current seek position. Bucket `k` covers distances in `[k/63 * chosen_max, (k+1)/63 * - /// chosen_max)`, with bucket 63 inclusive of `chosen_max`. Pixels with no observed - /// store→load pair (distance 0) are excluded. `chosen_max` is this Func's own max - /// (`PerFunc`) or the trace-wide max (`AcrossFuncs`) — matching whichever max drives - /// `to_rgba`'s color scale. pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { - let chosen_max = match normalization_mode { + let max = match normalization_mode { NormalizationMode::AcrossFuncs => self.global_max_reuse_distance, NormalizationMode::PerFunc => self.local_max_reuse_distance, }; let mut hist = vec![0u32; 64]; - if chosen_max > 0 { + if max > 0 { for &dist in &self.max_reuse_distance { if dist > 0 { - let bucket = ((dist as f64 / chosen_max as f64) * 63.0) as usize; + let bucket = ((dist as f64 / max as f64) * 63.0) as usize; hist[bucket.min(63)] += 1; } } } + hist } @@ -981,3 +877,249 @@ impl ReuseDistanceState { self.max_reuse_distance.clone() } } + +// ── NaN Rendering ──────────────────────────────────────────────────────────────────────────────── + +pub struct NaNState { + geom: FuncGeometry, + values: Vec, + nanbuffer: Vec, + applied_k: usize, +} + +impl NaNState { + pub fn new(trace: &Trace, func: &str) -> Option { + let geom = trace.func_geometry(func)?; + let nanbuffer = vec![0u8; geom.width * geom.height * geom.channels]; + let values = vec![0f64; geom.width * geom.height * geom.channels]; + + Some(Self { + geom, + values, + nanbuffer, + applied_k: 0, + }) + } + + fn apply_store(&mut self, pkt: &TracePacket) { + let FuncGeometry { + width, + height, + channels, + min_x, + min_y, + min_c, + .. + } = self.geom; + + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + Some((min_c, channels)), + |lane, _pixel_idx: usize, val_idx: usize, _x, _y| { + let Some(v) = pkt.decoded_value(lane) else { + return; + }; + + self.nanbuffer[val_idx] = if v.is_nan() { 1 } else { 0 }; + self.values[val_idx] = v; + }, + ); + } +} + +impl Renderer for NaNState { + type Value = f64; + + fn register(trace: &Trace, func: &str) -> Option { + Self::new(trace, func) + } + + fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + if target_k < self.applied_k { + self.values.iter_mut().for_each(|v| *v = 0.0); + self.nanbuffer.iter_mut().for_each(|b| *b = 0); + self.applied_k = 0; + } + + for &idx in &store_indices[self.applied_k..target_k] { + self.apply_store(&trace.packets[idx]); + } + + self.applied_k = target_k; + } + + fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + let mut out = vec![0u8; width * height * 4]; + let nb = &self.nanbuffer; + if channels >= 3 { + for (chunk, src) in out.chunks_exact_mut(4).zip(nb.chunks_exact(channels)) { + // If any channel is NaN, mark the pixel as cyan, otherwise transparent. + if src[0] == 1 || src[1] == 1 || src[2] == 1 { + chunk[0] = 0; + chunk[1] = 255; + chunk[2] = 255; + chunk[3] = 255; + } else { + chunk[0] = 0; + chunk[1] = 0; + chunk[2] = 0; + chunk[3] = 0; + } + } + } else { + for (chunk, src) in out.chunks_exact_mut(4).zip(nb.chunks_exact(channels)) { + // If the channel is NaN, mark the pixel as cyan, otherwise transparent. + if src[0] == 1 { + chunk[0] = 0; + chunk[1] = 255; + chunk[2] = 255; + chunk[3] = 255; + } else { + chunk[0] = 0; + chunk[1] = 0; + chunk[2] = 0; + chunk[3] = 0; + } + } + } + out + } + + fn to_values(&self) -> Vec { + self.values.clone() + } +} + +// ── Inf Rendering ──────────────────────────────────────────────────────────────────────────────── + +pub struct InfState { + geom: FuncGeometry, + values: Vec, + infbuffer: Vec, + applied_k: usize, +} + +impl InfState { + pub fn new(trace: &Trace, func: &str) -> Option { + let geom = trace.func_geometry(func)?; + let infbuffer = vec![0u8; geom.width * geom.height * geom.channels]; + let values = vec![0f64; geom.width * geom.height * geom.channels]; + + Some(Self { + geom, + values, + infbuffer, + applied_k: 0, + }) + } + + fn apply_store(&mut self, pkt: &TracePacket) { + let FuncGeometry { + width, + height, + channels, + min_x, + min_y, + min_c, + .. + } = self.geom; + + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + Some((min_c, channels)), + |lane, _pixel_idx: usize, val_idx: usize, _x, _y| { + let Some(v) = pkt.decoded_value(lane) else { + return; + }; + + self.infbuffer[val_idx] = if v.is_infinite() { 1 } else { 0 }; + self.values[val_idx] = v; + }, + ); + } +} + +impl Renderer for InfState { + type Value = f64; + + fn register(trace: &Trace, func: &str) -> Option { + Self::new(trace, func) + } + + fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { + let target_k = target_k.min(store_indices.len()); + if target_k < self.applied_k { + self.values.iter_mut().for_each(|v| *v = 0.0); + self.infbuffer.iter_mut().for_each(|b| *b = 0); + self.applied_k = 0; + } + + for &idx in &store_indices[self.applied_k..target_k] { + self.apply_store(&trace.packets[idx]); + } + + self.applied_k = target_k; + } + + fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + let mut out = vec![0u8; width * height * 4]; + let ib = &self.infbuffer; + if channels >= 3 { + for (chunk, src) in out.chunks_exact_mut(4).zip(ib.chunks_exact(channels)) { + // If any channel is inf, mark the pixel as magenta, otherwise transparent. + if src[0] == 1 || src[1] == 1 || src[2] == 1 { + chunk[0] = 255; + chunk[1] = 255; + chunk[2] = 0; + chunk[3] = 255; + } else { + chunk[0] = 0; + chunk[1] = 0; + chunk[2] = 0; + chunk[3] = 0; + } + } + } else { + for (chunk, src) in out.chunks_exact_mut(4).zip(ib.chunks_exact(channels)) { + // If the channel is inf, mark the pixel as magenta, otherwise transparent. + if src[0] == 1 { + chunk[0] = 255; + chunk[1] = 255; + chunk[2] = 0; + chunk[3] = 255; + } else { + chunk[0] = 0; + chunk[1] = 0; + chunk[2] = 0; + chunk[3] = 0; + } + } + } + out + } + + fn to_values(&self) -> Vec { + self.values.clone() + } +} diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 3ee734d24d43..ae013ec7c614 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -175,11 +175,11 @@ pub struct FuncStats { pub max_load_count: u32, /// Maximum number of redundant stores observed at any array / tensor coordinate for this Func. /// A store is considered redundant when the incoming value bit-matches the previously stored - /// value at that location. - pub max_redundant_count: u32, + /// value at that location AND there are no intervening loads from that location. + pub max_redundant_store_count: u32, /// Maximum store-to-load distance observed across all array / tensor coordinates for this Func. - /// Measured as thedifference in global packet indices between a store and the next load from - /// the same coordination. 0 when no store→load pair was observed. + /// Measured as the difference in global packet indices between a store and the next load from + /// the same coordination. pub max_reuse_distance: u64, } @@ -193,7 +193,7 @@ impl Default for FuncStats { max_value: None, max_store_count: 0, max_load_count: 0, - max_redundant_count: 0, + max_redundant_store_count: 0, max_reuse_distance: 0, } } @@ -210,7 +210,7 @@ pub struct FuncGeometry { pub min_c: i32, pub max_store_count: u32, pub max_load_count: u32, - pub max_redundant_count: u32, + pub max_redundant_store_count: u32, pub max_reuse_distance: u64, } @@ -324,14 +324,20 @@ fn update_value_range(pkt: &TracePacket, stats: &mut FuncStats) { if let Some(v) = pkt.decoded_value(i) { match (stats.min_value, stats.max_value) { (None, _) => { + // Ensure we do not initialize min/max with NaN or Inf. + if v.is_nan() || v.is_infinite() { + continue; + } + stats.min_value = Some(v); stats.max_value = Some(v); } (Some(mn), Some(mx)) => { - if v < mn { + if v < mn && v.is_finite() { stats.min_value = Some(v); } - if v > mx { + + if v > mx && v.is_finite() { stats.max_value = Some(v); } } @@ -709,7 +715,7 @@ impl Trace { w, h, None, - |_lane, pixel_idx, _val_idx| { + |_lane, pixel_idx, _val_idx, _x, _y| { counts[pixel_idx] += 1; }, ); @@ -733,7 +739,7 @@ impl Trace { w, h, None, - |_lane, pixel_idx, _val_idx| { + |_lane, pixel_idx, _val_idx, _x, _y| { counts[pixel_idx] += 1; }, ); @@ -788,7 +794,7 @@ impl Trace { w, h, Some((min_c, channels)), - |lane, pixel_idx, val_idx| { + |lane, pixel_idx, val_idx, _x, _y| { let Some(v) = pkt.decoded_value(lane) else { return; }; @@ -813,7 +819,7 @@ impl Trace { w, h, Some((min_c, channels)), - |_lane, _pixel_idx, val_idx| { + |_lane, _pixel_idx, val_idx, _x, _y| { last_values[val_idx] = None; }, ); @@ -821,7 +827,8 @@ impl Trace { } if let Some(stats) = funcs.get_mut(func_name.as_str()) { - stats.max_redundant_count = redundant_counts.iter().copied().max().unwrap_or(0); + stats.max_redundant_store_count = + redundant_counts.iter().copied().max().unwrap_or(0); } } } @@ -875,7 +882,7 @@ impl Trace { w, h, Some((min_c, channels)), - |_lane, _pixel_idx, val_idx| { + |_lane, _pixel_idx, val_idx, _x, _y| { last_store_at[val_idx] = global_idx; }, ); @@ -890,7 +897,7 @@ impl Trace { w, h, Some((min_c, channels)), - |_lane, pixel_idx, val_idx| { + |_lane, pixel_idx, val_idx, _x, _y| { if last_store_at[val_idx] != usize::MAX { let dist = (global_idx - last_store_at[val_idx]) as u64; if dist > max_reuse_distances[pixel_idx] { @@ -940,7 +947,7 @@ impl Trace { w, h, Some((min_c, channels)), - |_lane, pixel_idx, val_idx| { + |_lane, pixel_idx, val_idx, _x, _y| { if first_load_at[val_idx] == usize::MAX { first_load_at[val_idx] = global_idx; } else { @@ -1029,7 +1036,7 @@ impl Trace { min_c, max_store_count: stats.max_store_count, max_load_count: stats.max_load_count, - max_redundant_count: stats.max_redundant_count, + max_redundant_store_count: stats.max_redundant_store_count, max_reuse_distance: stats.max_reuse_distance, }) } @@ -1087,14 +1094,14 @@ pub(crate) fn pixel_xy( /// When `channel` is `Some((min_c, channels))`, lanes are additionally filtered to those whose /// channel coordinate falls within `0..channels`, and `val_idx` is the flattened /// `pixel_idx * channels + c` location; otherwise `val_idx` is just `pixel_idx`. -fn for_each_lane_pixel( +pub fn for_each_lane_pixel( pkt: &TracePacket, min_x: i32, min_y: i32, w: usize, h: usize, channel: Option<(i32, usize)>, - mut f: impl FnMut(usize, usize, usize), + mut f: impl FnMut(usize, usize, usize, i32, i32), ) { let n_lanes = pkt.type_.lanes.max(1) as usize; let dims_per_lane = pkt.coordinates.len() / n_lanes; @@ -1121,6 +1128,6 @@ fn for_each_lane_pixel( pixel_idx }; - f(lane, pixel_idx, val_idx); + f(lane, pixel_idx, val_idx, x, y); } } diff --git a/apps/halidoscope/src/App.css b/apps/halidoscope/src/App.css index 523ec934d815..39e76d3be5e5 100644 --- a/apps/halidoscope/src/App.css +++ b/apps/halidoscope/src/App.css @@ -46,6 +46,14 @@ input[type="number"] { --text-tiny: 0.625rem; --text-tiny--line-height: 1.5; + + --animate-blink: blink 1s step-end infinite; + + @keyframes blink { + 50% { + opacity: 0; + } + } } @layer components { diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index b6d6009e25be..55d22c653147 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -30,12 +30,18 @@ import { renderRedundantStores, renderReuseDistance, type RenderResult, + renderNaN, + renderInf, } from "@/utils/api"; import { isFuncBufferLive, isEdgeLive } from "@/utils/liveness"; +import { nanAtom } from "@/state/nan"; +import { infAtom } from "@/state/inf"; function FuncNode({ data }: NodeProps>) { const { name, width, height } = data; const canvasRef = React.useRef(null); + const nanOverlayRef = React.useRef(null); + const infOverlayRef = React.useRef(null); const { funcs } = useTraceContext(); const liveness = useAtomValue(livenessAtom); @@ -43,6 +49,8 @@ function FuncNode({ data }: NodeProps>) { const render = useAtomValue(renderAtom); const activeFunc = useAtomValue(funcAtom); const setHistogramData = useSetAtom(histogramAtom); + const nan = useAtomValue(nanAtom); + const inf = useAtomValue(infAtom); const nodes = useNodes(); const edges = useEdges(); @@ -185,6 +193,82 @@ function FuncNode({ data }: NodeProps>) { draw(); }, [packetIndex, name, width, height, render, activeFunc, setHistogramData]); + React.useEffect(() => { + latestIndexRef.current = packetIndex; + + if (renderingRef.current) { + return; + } + + async function drawNaN() { + try { + while (true) { + const target = latestIndexRef.current; + + const result = await renderNaN( + name, + packetIndex, + render.normalizationMode, + ); + + const ctx = nanOverlayRef.current?.getContext("2d"); + + if (ctx) { + ctx.putImageData(new ImageData(result.pixels, width, height), 0, 0); + } + + if (latestIndexRef.current === target) { + break; + } + } + } catch { + console.error( + `Failed to render NaN overlay for ${name} at index ${latestIndexRef.current}`, + ); + } + } + + drawNaN(); + }, [name, packetIndex, render.normalizationMode, width, height]); + + React.useEffect(() => { + latestIndexRef.current = packetIndex; + + if (renderingRef.current) { + return; + } + + async function drawInf() { + try { + while (true) { + const target = latestIndexRef.current; + + const result = await renderInf( + name, + packetIndex, + render.normalizationMode, + ); + + const ctx = infOverlayRef.current?.getContext("2d"); + + if (ctx) { + ctx.putImageData(new ImageData(result.pixels, width, height), 0, 0); + } + + if (latestIndexRef.current === target) { + break; + } + } + } catch { + console.error( + `Failed to render Inf overlay for ${name} at index ${latestIndexRef.current}`, + ); + } + } + + drawInf(); + }, [name, packetIndex, render.normalizationMode, width, height]); + return ( <> >) {
>) { "ring-1": zoom >= 1, })} /> + +
{incomingEdgeCount > 0 && edges.every((edge) => !edge.hidden) ? ( }) { > Visualization + + Debug + @@ -33,6 +40,9 @@ function ControlTabs({ funcs }: { funcs: Record }) { + + +
); diff --git a/apps/halidoscope/src/components/controls/DebugPanel.tsx b/apps/halidoscope/src/components/controls/DebugPanel.tsx new file mode 100644 index 000000000000..5f5ac210decd --- /dev/null +++ b/apps/halidoscope/src/components/controls/DebugPanel.tsx @@ -0,0 +1,21 @@ +import { Separator } from "radix-ui"; + +import ControlSection from "@/components/controls/ControlSection"; +import InfControls from "@/components/controls/inf/InfControls"; +import NaNControls from "@/components/controls/nan/NaNControls"; + +function DebugPanel() { + return ( +
+ + + + + + + +
+ ); +} + +export default DebugPanel; diff --git a/apps/halidoscope/src/components/controls/inf/InfControls.tsx b/apps/halidoscope/src/components/controls/inf/InfControls.tsx new file mode 100644 index 000000000000..9d1b2edb6cf9 --- /dev/null +++ b/apps/halidoscope/src/components/controls/inf/InfControls.tsx @@ -0,0 +1,76 @@ +import { useAtom } from "jotai"; +import { Checkbox, Label, Select } from "radix-ui"; + +import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; +import CheckIcon from "@/components/icons/CheckIcon"; +import { infAtom } from "@/state/inf"; +import type { AnimationMode } from "@/types"; +import { ANIMATION_MODES } from "@/utils/constants"; + +function InfControls() { + const [inf, setInf] = useAtom(infAtom); + + return ( +
+
+ { + setInf({ ...inf, active: !!checked }); + }} + > + + + + + +
+ {inf.active ? ( +
+ + Animation Mode + + + setInf({ ...inf, animationMode: value as AnimationMode }) + } + > + + + + + + + + + {ANIMATION_MODES.map((value) => ( + + {value} + + ))} + + + +
+ ) : null} +
+ ); +} + +export default InfControls; diff --git a/apps/halidoscope/src/components/controls/nan/NaNControls.tsx b/apps/halidoscope/src/components/controls/nan/NaNControls.tsx new file mode 100644 index 000000000000..f348e96c6d42 --- /dev/null +++ b/apps/halidoscope/src/components/controls/nan/NaNControls.tsx @@ -0,0 +1,76 @@ +import { useAtom } from "jotai"; +import { Checkbox, Label, Select } from "radix-ui"; + +import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; +import CheckIcon from "@/components/icons/CheckIcon"; +import { nanAtom } from "@/state/nan"; +import type { AnimationMode } from "@/types"; +import { ANIMATION_MODES } from "@/utils/constants"; + +function NaNControls() { + const [nan, setNan] = useAtom(nanAtom); + + return ( +
+
+ { + setNan({ ...nan, active: !!checked }); + }} + > + + + + + +
+ {nan.active ? ( +
+ + Animation Mode + + + setNan({ ...nan, animationMode: value as AnimationMode }) + } + > + + + + + + + + + {ANIMATION_MODES.map((value) => ( + + {value} + + ))} + + + +
+ ) : null} +
+ ); +} + +export default NaNControls; diff --git a/apps/halidoscope/src/components/controls/render/RenderMode.tsx b/apps/halidoscope/src/components/controls/render/RenderMode.tsx index 7fa1db76684c..8b2edd73a6d4 100644 --- a/apps/halidoscope/src/components/controls/render/RenderMode.tsx +++ b/apps/halidoscope/src/components/controls/render/RenderMode.tsx @@ -1,6 +1,7 @@ import { Select } from "radix-ui"; import { useAtom } from "jotai"; +import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; import { renderAtom, RENDER_MODES, type RenderMode } from "@/state/render"; function VisualizationSelect() { @@ -19,20 +20,7 @@ function VisualizationSelect() { > - - - + ({ + active: false, + animationMode: "Blink", +}); diff --git a/apps/halidoscope/src/state/nan.ts b/apps/halidoscope/src/state/nan.ts new file mode 100644 index 000000000000..e21caa1bdc57 --- /dev/null +++ b/apps/halidoscope/src/state/nan.ts @@ -0,0 +1,8 @@ +import { atom } from "jotai"; + +import { AnimationMode } from "@/types"; + +export const nanAtom = atom<{ active: boolean; animationMode: AnimationMode }>({ + active: false, + animationMode: "Blink", +}); diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts index 268e01ed1733..df6936384e0f 100644 --- a/apps/halidoscope/src/types/index.ts +++ b/apps/halidoscope/src/types/index.ts @@ -21,7 +21,7 @@ export interface FuncMeta extends Record { max_value: number | null; max_store_count: number; max_load_count: number; - max_redundant_count: number; + max_redundant_store_count: number; max_reuse_distance: number; buffer_liveness: IndexRange; produce_ranges: IndexRange[]; @@ -39,3 +39,5 @@ export interface TraceMeta { export type NodeTypes = "funcNode"; export type EdgeTypes = "funcEdge"; + +export type AnimationMode = "Blink" | "Pulse" | "None"; diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index b51a2602fedc..48d9d5ad35a3 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -23,7 +23,10 @@ export async function renderGrayscale( normalizationMode, }); - return { pixels: new Uint8ClampedArray(buffer), histogram: null }; + return { + pixels: new Uint8ClampedArray(buffer), + histogram: null, + }; } export async function renderRgb( @@ -37,7 +40,10 @@ export async function renderRgb( normalizationMode, }); - return { pixels: new Uint8ClampedArray(buffer), histogram: null }; + return { + pixels: new Uint8ClampedArray(buffer), + histogram: null, + }; } // The backend appends the histogram's bins as little-endian u32s directly after the pixel @@ -118,3 +124,37 @@ export async function renderReuseDistance( return splitPixelsAndHistogram(buffer, width, height); } + +export async function renderNaN( + func: string, + globalIndex: number, + normalizationMode: NormalizationMode, +): Promise { + const buffer = await invoke("render_nan", { + func, + globalIndex, + normalizationMode, + }); + + return { + pixels: new Uint8ClampedArray(buffer), + histogram: null, + }; +} + +export async function renderInf( + func: string, + globalIndex: number, + normalizationMode: NormalizationMode, +): Promise { + const buffer = await invoke("render_inf", { + func, + globalIndex, + normalizationMode, + }); + + return { + pixels: new Uint8ClampedArray(buffer), + histogram: null, + }; +} diff --git a/apps/halidoscope/src/utils/constants.ts b/apps/halidoscope/src/utils/constants.ts index 96a58584a69e..9d00a2ef6c27 100644 --- a/apps/halidoscope/src/utils/constants.ts +++ b/apps/halidoscope/src/utils/constants.ts @@ -1,3 +1,7 @@ +import { AnimationMode } from "@/types"; + export const DEFAULT_PLAYBACK_RATE = 10000; /** Debounce window in ms before a settled scrub position is rendered. */ export const SCRUB_DEBOUNCE_MS = 50; + +export const ANIMATION_MODES: AnimationMode[] = ["Blink", "Pulse", "None"]; From ee89272e402b5da1b3f78bd4185ddd31067b8645 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 21 Jul 2026 10:12:43 -0700 Subject: [PATCH 27/67] Add support for Thread Coverage rendering. --- apps/halidoscope/src-tauri/src/commands.rs | 75 +++++- apps/halidoscope/src-tauri/src/lib.rs | 1 + apps/halidoscope/src-tauri/src/render.rs | 235 +++++++++++++++++- apps/halidoscope/src-tauri/src/trace.rs | 52 +++- .../src/components/canvas/FuncNode.tsx | 45 +++- .../controls/VisualizationPanel.tsx | 60 ++++- .../controls/bar-chart/BarChart.tsx | 78 ++++++ .../controls/bar-chart/BarChartParameters.tsx | 107 ++++++++ .../histogram/HistogramParameters.tsx | 25 +- .../components/controls/inf/InfControls.tsx | 2 +- .../components/controls/nan/NaNControls.tsx | 2 +- apps/halidoscope/src/state/render.ts | 1 + apps/halidoscope/src/state/thread.ts | 7 + apps/halidoscope/src/types/index.ts | 1 + apps/halidoscope/src/utils/api.ts | 32 +++ 15 files changed, 666 insertions(+), 57 deletions(-) create mode 100644 apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx create mode 100644 apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx create mode 100644 apps/halidoscope/src/state/thread.ts diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index b121cff36843..b2be3aba8844 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -2,7 +2,7 @@ //! //! This module owns the types that cross the Tauri IPC boundary. -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, HashMap}; use std::sync::Mutex; use serde::Serialize; @@ -11,7 +11,7 @@ use tauri::State; use crate::render::{ GrayscaleState, InfState, LoadFrequencyState, NaNState, NormalizationMode, RedundantState, - Renderer, ReuseDistanceState, RgbState, StoreFrequencyState, + Renderer, ReuseDistanceState, RgbState, StoreFrequencyState, ThreadOpMode, ThreadState, }; use crate::trace::Trace; @@ -48,6 +48,7 @@ pub struct FuncMeta { pub produce_ranges: Vec, pub consume_ranges: Vec, pub thread_count: u32, + pub thread_ids: Vec, } /// Top-level payload returned by `open_trace`. @@ -114,11 +115,16 @@ impl TraceMeta { .copied() .map(IndexRange::from_tuple) .collect(), - thread_count: (trace + // A missing entry means `name` ran entirely serially (never inside a + // `BeginParallelTask`), not that it has no threads; default to the implicit + // serial thread `{0}` so `thread_ids` and `thread_count` agree. + thread_count: trace .func_thread_ids(name) - .unwrap_or(&BTreeSet::new()) - .len() as u32) - .max(1), + .map_or(1, |ids| ids.len() as u32), + thread_ids: trace + .func_thread_ids(name) + .map(|ids| ids.iter().copied().collect()) + .unwrap_or_else(|| vec![0]), } }) .collect(); @@ -152,6 +158,7 @@ struct Loaded { reuse_distance_renderers: HashMap, nan_renderers: HashMap, inf_renderers: HashMap, + thread_renderers: HashMap, } /// App-wide state managed by Tauri. A single trace is loaded at a time; opening a new one replaces @@ -172,6 +179,24 @@ fn pack_pixels_and_histogram(mut pixels: Vec, histogram: Vec) -> Vec, + store_counts: &[u32], + load_counts: &[u32], +) -> Vec { + pixels.reserve((store_counts.len() + load_counts.len()) * 4); + for &c in store_counts { + pixels.extend_from_slice(&c.to_le_bytes()); + } + for &c in load_counts { + pixels.extend_from_slice(&c.to_le_bytes()); + } + pixels +} + // ── Commands ────────────────────────────────────────────────────────────────── /// Parses a `.hltrace` file and returns the metadata the frontend needs to set up canvases and @@ -192,6 +217,7 @@ pub fn open_trace(path: String, state: State) -> Result, +) -> Result { + let mut guard = state.inner.lock().map_err(|e| e.to_string())?; + let loaded = guard.as_mut().ok_or("no trace loaded")?; + let Loaded { + trace, + thread_renderers, + .. + } = loaded; + + if !thread_renderers.contains_key(&func) { + let rs = ThreadState::new(trace, &func) + .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; + thread_renderers.insert(func.clone(), rs); + } + let renderer = thread_renderers.get_mut(&func).expect("just inserted"); + let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); + let load_indices = trace.func_load_indices(&func).unwrap_or(&[]); + let store_k = store_indices.partition_point(|&p| p <= global_index as usize); + let load_k = load_indices.partition_point(|&p| p <= global_index as usize); + renderer.seek(trace, store_indices, load_indices, store_k, load_k, op_mode); + + let pixels = renderer.to_rgba(normalization_mode); + let (store_counts, load_counts) = renderer.to_thread_counts(); + Ok(Response::new(pack_pixels_and_thread_counts( + pixels, + store_counts, + load_counts, + ))) +} diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index b001d0bd35ea..fc07833ccdd9 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -60,6 +60,7 @@ pub fn run() { commands::render_reuse_distance, commands::render_nan, commands::render_inf, + commands::render_thread ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index 16330ffa2d67..5eb54c7731e9 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -1,3 +1,5 @@ +use std::vec; + use ::colorous; use serde::Deserialize; @@ -69,7 +71,7 @@ impl GrayscaleState { width, height, Some((min_c, channels)), - |lane, _pixel_idx, val_idx, _x, _y| { + |lane, _pixel_idx, val_idx| { let Some(v) = pkt.decoded_value(lane) else { return; }; @@ -188,7 +190,7 @@ impl RgbState { width, height, Some((min_c, channels)), - |lane, _pixel_idx, val_idx, _x, _y| { + |lane, _pixel_idx, val_idx| { let Some(v) = pkt.decoded_value(lane) else { return; }; @@ -427,7 +429,7 @@ impl LoadFrequencyState { width, height, None, - |_lane, pixel_idx, _val_idx, _x, _y| { + |_lane, pixel_idx, _val_idx| { self.counts[pixel_idx] += 1; }, ); @@ -563,7 +565,7 @@ impl RedundantState { width, height, Some((min_c, channels)), - |lane, pixel_idx: usize, val_idx: usize, _x, _y| { + |lane, pixel_idx: usize, val_idx: usize| { let Some(v) = pkt.decoded_value(lane) else { return; }; @@ -773,7 +775,7 @@ impl ReuseDistanceState { width, height, Some((min_c, channels)), - |_lane, _pixel_idx, val_idx, _x, _y| { + |_lane, _pixel_idx, val_idx| { self.anchor_at[val_idx] = global_idx; }, ); @@ -797,7 +799,7 @@ impl ReuseDistanceState { width, height, Some((min_c, channels)), - |_lane, pixel_idx, val_idx, _x, _y| { + |_lane, pixel_idx, val_idx| { if self.is_input { // First load is the free memcpy; establish the anchor and record no distance. // Subsequent loads to the same location measure from that first load. @@ -919,7 +921,7 @@ impl NaNState { width, height, Some((min_c, channels)), - |lane, _pixel_idx: usize, val_idx: usize, _x, _y| { + |lane, _pixel_idx: usize, val_idx: usize| { let Some(v) = pkt.decoded_value(lane) else { return; }; @@ -1042,7 +1044,7 @@ impl InfState { width, height, Some((min_c, channels)), - |lane, _pixel_idx: usize, val_idx: usize, _x, _y| { + |lane, _pixel_idx: usize, val_idx: usize| { let Some(v) = pkt.decoded_value(lane) else { return; }; @@ -1123,3 +1125,220 @@ impl Renderer for InfState { self.values.clone() } } + +// ── Thread Rendering ───────────────────────────────────────────────────────────────────────────── + +#[derive(Deserialize, Clone, Copy, PartialEq)] +pub enum ThreadOpMode { + Store, + Load, + All, +} + +pub struct ThreadState { + geom: FuncGeometry, + thread_ids: Vec, + thread_id_buffer: Vec, + store_counts: Vec, + load_counts: Vec, + applied_store_k: usize, + applied_load_k: usize, + applied_op_mode: Option, +} + +impl ThreadState { + pub fn new(trace: &Trace, func: &str) -> Option { + let geom = trace.func_geometry(func)?; + // A missing entry means this Func was never realized inside a `BeginParallelTask` (i.e. + // it ran entirely serially) — every packet's `thread_id` defaults to 0 in that case. + let thread_ids = trace + .func_thread_ids(func) + .map(|ids| ids.iter().map(|&id| id as i32).collect::>()) + .unwrap_or_else(|| vec![0]); + let n_threads = thread_ids.len(); + + // `i32::MIN` marks a pixel no store/load has touched yet. Plain `0` would collide with a + // real thread ID, making untouched pixels indistinguishable from ones actually written by + // thread 0. + let thread_id_buffer = vec![i32::MIN; geom.width * geom.height]; + + Some(Self { + geom, + thread_ids, + thread_id_buffer, + store_counts: vec![0u32; n_threads], + load_counts: vec![0u32; n_threads], + applied_store_k: 0, + applied_load_k: 0, + applied_op_mode: None, + }) + } + + fn reset(&mut self) { + self.thread_id_buffer.iter_mut().for_each(|v| *v = i32::MIN); + self.store_counts.iter_mut().for_each(|c| *c = 0); + self.load_counts.iter_mut().for_each(|c| *c = 0); + self.applied_store_k = 0; + self.applied_load_k = 0; + } + + fn apply_store(&mut self, pkt: &TracePacket) { + let FuncGeometry { + width, + height, + min_x, + min_y, + .. + } = self.geom; + let thread_idx = self.thread_ids.binary_search(&pkt.thread_id).ok(); + + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + None, + |lane, pixel_idx: usize, _val_idx: usize| { + let Some(_v) = pkt.decoded_value(lane) else { + return; + }; + + self.thread_id_buffer[pixel_idx] = pkt.thread_id; + if let Some(i) = thread_idx { + self.store_counts[i] += 1; + } + }, + ); + } + + fn apply_load(&mut self, pkt: &TracePacket) { + let FuncGeometry { + width, + height, + min_x, + min_y, + .. + } = self.geom; + let thread_idx = self.thread_ids.binary_search(&pkt.thread_id).ok(); + + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + None, + |lane, pixel_idx: usize, _val_idx: usize| { + let Some(_v) = pkt.decoded_value(lane) else { + return; + }; + + self.thread_id_buffer[pixel_idx] = pkt.thread_id; + if let Some(i) = thread_idx { + self.load_counts[i] += 1; + } + }, + ); + } + + pub fn seek( + &mut self, + trace: &Trace, + store_indices: &[usize], + load_indices: &[usize], + target_store_k: usize, + target_load_k: usize, + op_mode: ThreadOpMode, + ) { + let target_store_k = target_store_k.min(store_indices.len()); + let target_load_k = target_load_k.min(load_indices.len()); + + if target_store_k < self.applied_store_k + || target_load_k < self.applied_load_k + || self.applied_op_mode != Some(op_mode) + { + self.reset(); + } + + let store_slice = &store_indices[self.applied_store_k..target_store_k]; + let load_slice = &load_indices[self.applied_load_k..target_load_k]; + let mut si = 0; + let mut li = 0; + + while si < store_slice.len() || li < load_slice.len() { + let next_is_store = si < store_slice.len() + && (li >= load_slice.len() || store_slice[si] < load_slice[li]); + + match (&op_mode, next_is_store) { + // op_mode is Store and the next packet is a store. + (ThreadOpMode::Store, true) => { + self.apply_store(&trace.packets[store_slice[si]]); + si += 1; + } + // op_mode is Load and the next packet is a load. + (ThreadOpMode::Load, false) => { + self.apply_load(&trace.packets[load_slice[li]]); + li += 1; + } + // op_mode is All and the next packet is a store. + (ThreadOpMode::All, true) => { + self.apply_store(&trace.packets[store_slice[si]]); + si += 1; + } + // op_mode is All and the next packet is a load. + (ThreadOpMode::All, false) => { + self.apply_load(&trace.packets[load_slice[li]]); + li += 1; + } + (_, true) => { + // Increment si even if we don't apply the store, to keep the merge moving forward. + si += 1; + } + (_, false) => { + // Increment li even if we don't apply the load, to keep the merge moving forward. + li += 1; + } + } + } + + self.applied_store_k = target_store_k; + self.applied_load_k = target_load_k; + self.applied_op_mode = Some(op_mode); + } + + pub fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { + let FuncGeometry { width, height, .. } = self.geom; + let domain = &self.thread_ids; + let range = colorous::TABLEAU10; + + let mut out = vec![0u8; width * height * 4]; + + for (chunk, &thread_id) in out.chunks_exact_mut(4).zip(self.thread_id_buffer.iter()) { + let idx = domain.binary_search(&thread_id); + + if let Ok(i) = idx { + let color = range[i]; + chunk[0] = color.r; + chunk[1] = color.g; + chunk[2] = color.b; + chunk[3] = 255; + } else { + chunk[0] = 0; + chunk[1] = 0; + chunk[2] = 0; + chunk[3] = 255; + } + } + + out + } + + pub fn to_values(&self) -> Vec { + self.thread_id_buffer.clone() + } + + pub fn to_thread_counts(&self) -> (&[u32], &[u32]) { + (&self.store_counts, &self.load_counts) + } +} diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index ae013ec7c614..2297ea2eaed6 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -101,8 +101,11 @@ pub struct TracePacket { pub value_index: i32, /// Only meaningful for load/store events. pub type_: HalideType, - /// The Halide-internal thread that executed a parallel task. Only meaningful for - /// `BeginParallelTask`; zero for every other event. + /// The Halide-internal thread that executed this event. Parsed directly off + /// `BeginParallelTask` packets; for Load/Store packets (whose header reuses this byte offset + /// for `type_` in the raw binary format) it's resolved after parsing by walking the parent + /// chain up to the nearest enclosing `BeginParallelTask` (0 if there is none, i.e. serial + /// execution). Meaningless for every other event. pub thread_id: i32, /// Coordinates in dim-major / lane-minor order: [x₀..xₙ, y₀..yₙ, c₀..cₙ] where n = type_.lanes. pub coordinates: Vec, @@ -434,6 +437,12 @@ impl Trace { // parsed. let mut id_to_info: HashMap = HashMap::new(); + // BeginParallelTask's own id -> the thread_id it carries. Load/Store packets don't carry + // a real thread_id (see TracePacket::thread_id), so resolving the thread that executed a + // given load/store requires walking its parent chain up to the nearest BeginParallelTask + // and looking up its thread_id here. + let mut thread_id_by_task_id: HashMap = HashMap::new(); + // Loads we deferred for DAG inference. let mut pending_loads: Vec<(String, i32)> = Vec::new(); @@ -649,6 +658,7 @@ impl Trace { } EventCode::BeginParallelTask => { pending_parallel_tasks.push((thread_id, parent_id)); + thread_id_by_task_id.insert(id, thread_id); } _ => {} } @@ -699,6 +709,26 @@ impl Trace { } } + // Resolve the executing thread for each load/store packet by walking its parent chain. + for pkt in packets.iter_mut() { + if !pkt.is_load_or_store() { + continue; + } + + let mut current = pkt.parent_id; + loop { + if let Some(&tid) = thread_id_by_task_id.get(¤t) { + pkt.thread_id = tid; + break; + } + + match id_to_info.get(¤t) { + Some((_, _, next_parent)) => current = *next_parent, + None => break, + } + } + } + // Compute max per-pixel store/load counts for each Func using the index lists. We extract // extents first (shared borrow) then write back (mut borrow) to keep the two borrows of // `funcs` non-overlapping. @@ -715,7 +745,7 @@ impl Trace { w, h, None, - |_lane, pixel_idx, _val_idx, _x, _y| { + |_lane, pixel_idx, _val_idx| { counts[pixel_idx] += 1; }, ); @@ -739,7 +769,7 @@ impl Trace { w, h, None, - |_lane, pixel_idx, _val_idx, _x, _y| { + |_lane, pixel_idx, _val_idx| { counts[pixel_idx] += 1; }, ); @@ -794,7 +824,7 @@ impl Trace { w, h, Some((min_c, channels)), - |lane, pixel_idx, val_idx, _x, _y| { + |lane, pixel_idx, val_idx| { let Some(v) = pkt.decoded_value(lane) else { return; }; @@ -819,7 +849,7 @@ impl Trace { w, h, Some((min_c, channels)), - |_lane, _pixel_idx, val_idx, _x, _y| { + |_lane, _pixel_idx, val_idx| { last_values[val_idx] = None; }, ); @@ -882,7 +912,7 @@ impl Trace { w, h, Some((min_c, channels)), - |_lane, _pixel_idx, val_idx, _x, _y| { + |_lane, _pixel_idx, val_idx| { last_store_at[val_idx] = global_idx; }, ); @@ -897,7 +927,7 @@ impl Trace { w, h, Some((min_c, channels)), - |_lane, pixel_idx, val_idx, _x, _y| { + |_lane, pixel_idx, val_idx| { if last_store_at[val_idx] != usize::MAX { let dist = (global_idx - last_store_at[val_idx]) as u64; if dist > max_reuse_distances[pixel_idx] { @@ -947,7 +977,7 @@ impl Trace { w, h, Some((min_c, channels)), - |_lane, pixel_idx, val_idx, _x, _y| { + |_lane, pixel_idx, val_idx| { if first_load_at[val_idx] == usize::MAX { first_load_at[val_idx] = global_idx; } else { @@ -1101,7 +1131,7 @@ pub fn for_each_lane_pixel( w: usize, h: usize, channel: Option<(i32, usize)>, - mut f: impl FnMut(usize, usize, usize, i32, i32), + mut f: impl FnMut(usize, usize, usize), ) { let n_lanes = pkt.type_.lanes.max(1) as usize; let dims_per_lane = pkt.coordinates.len() / n_lanes; @@ -1128,6 +1158,6 @@ pub fn for_each_lane_pixel( pixel_idx }; - f(lane, pixel_idx, val_idx, x, y); + f(lane, pixel_idx, val_idx); } } diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 55d22c653147..0aa1be350ccc 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -18,9 +18,12 @@ import HandleCircle from "@/components/canvas/HandleCircle"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; import { histogramAtom } from "@/state/histogram"; +import { infAtom } from "@/state/inf"; import { livenessAtom } from "@/state/liveness"; import { packetAtom } from "@/state/packet"; +import { nanAtom } from "@/state/nan"; import { renderAtom } from "@/state/render"; +import { threadAtom } from "@/state/thread"; import type { FuncMeta } from "@/types"; import { renderGrayscale, @@ -32,10 +35,9 @@ import { type RenderResult, renderNaN, renderInf, + renderThread, } from "@/utils/api"; import { isFuncBufferLive, isEdgeLive } from "@/utils/liveness"; -import { nanAtom } from "@/state/nan"; -import { infAtom } from "@/state/inf"; function FuncNode({ data }: NodeProps>) { const { name, width, height } = data; @@ -51,6 +53,7 @@ function FuncNode({ data }: NodeProps>) { const setHistogramData = useSetAtom(histogramAtom); const nan = useAtomValue(nanAtom); const inf = useAtomValue(infAtom); + const thread = useAtomValue(threadAtom); const nodes = useNodes(); const edges = useEdges(); @@ -164,6 +167,17 @@ function FuncNode({ data }: NodeProps>) { width, height, ); + break; + case "Thread Coverage": + result = await renderThread( + name, + target, + render.normalizationMode, + thread.op, + width, + height, + ); + break; } @@ -183,17 +197,30 @@ function FuncNode({ data }: NodeProps>) { break; } } - } catch { + } catch (err) { console.error( - `Failed to render ${name} at index ${latestIndexRef.current}`, + `Failed to render ${name} at index ${latestIndexRef.current}: ${err}`, ); } } draw(); - }, [packetIndex, name, width, height, render, activeFunc, setHistogramData]); + }, [ + packetIndex, + name, + width, + height, + render, + activeFunc, + setHistogramData, + thread.op, + ]); React.useEffect(() => { + if (!nan.active) { + return; + } + latestIndexRef.current = packetIndex; if (renderingRef.current) { @@ -229,9 +256,13 @@ function FuncNode({ data }: NodeProps>) { } drawNaN(); - }, [name, packetIndex, render.normalizationMode, width, height]); + }, [nan.active, name, packetIndex, render.normalizationMode, width, height]); React.useEffect(() => { + if (!inf.active) { + return; + } + latestIndexRef.current = packetIndex; if (renderingRef.current) { @@ -267,7 +298,7 @@ function FuncNode({ data }: NodeProps>) { } drawInf(); - }, [name, packetIndex, render.normalizationMode, width, height]); + }, [inf.active, name, packetIndex, render.normalizationMode, width, height]); return ( <> diff --git a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx index 620341b92bfa..6a8f90f0528b 100644 --- a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -3,16 +3,19 @@ import { Separator } from "radix-ui"; import * as React from "react"; import ControlSection from "@/components/controls/ControlSection"; +import BarChart from "@/components/controls/bar-chart/BarChart"; +import BarChartParameters from "@/components/controls/bar-chart/BarChartParameters"; import GraphDisplay from "@/components/controls/graph/GraphDisplay"; +import Histogram from "@/components/controls/histogram/Histogram"; +import HistogramParameters from "@/components/controls/histogram/HistogramParameters"; import LivenessControls from "@/components/controls/liveness/LivenessControls"; import PlaybackRate from "@/components/controls/playback/PlaybackRate"; import RenderMode from "@/components/controls/render/RenderMode"; -import Histogram from "@/components/controls/histogram/Histogram"; -import HistogramSelect from "@/components/controls/histogram/HistogramParameters"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; import { histogramAtom } from "@/state/histogram"; import { type RenderMode as RM, renderAtom } from "@/state/render"; +import { threadAtom } from "@/state/thread"; const HISTOGRAM_RENDER_MODES = new Set([ "Store Frequency", @@ -21,6 +24,8 @@ const HISTOGRAM_RENDER_MODES = new Set([ "Reuse Distance", ]); +const BAR_CHART_RENDER_MODES = new Set(["Thread Coverage"]); + const RENDER_MODE_TO_LABEL: Record = { Grayscale: "", RGB: "", @@ -28,6 +33,7 @@ const RENDER_MODE_TO_LABEL: Record = { "Load Frequency": "Load Count", "Redundant Stores": "Redundant Store Count", "Reuse Distance": "Reuse Distance (Packets)", + "Thread Coverage": "Thread ID", }; function VisualizationPanel() { @@ -35,6 +41,7 @@ function VisualizationPanel() { const render = useAtomValue(renderAtom); const activeFunc = useAtomValue(funcAtom); const { data, scale } = useAtomValue(histogramAtom); + const thread = useAtomValue(threadAtom); const hasHistogram = HISTOGRAM_RENDER_MODES.has(render.renderMode) && @@ -92,6 +99,37 @@ function VisualizationPanel() { globalMaxReuseDistance, ]); + const hasBarChart = + BAR_CHART_RENDER_MODES.has(render.renderMode) && + activeFunc && + funcs[activeFunc] && + data !== null; + + const { data: barChartData } = React.useMemo((): { + data: { x: string; y: number }[]; + } => { + if (!hasBarChart || !data) { + return { data: [] }; + } + + switch (render.renderMode) { + case "Thread Coverage": { + const threadIds = funcs[activeFunc].thread_ids; + const storeCounts = data.slice(0, threadIds.length); + const loadCounts = data.slice(threadIds.length, threadIds.length * 2); + + return { + data: threadIds.map((threadId, i) => ({ + x: `${threadId}`, + y: thread.op === "Store" ? storeCounts[i] : loadCounts[i], + })), + }; + } + default: + return { data: [] }; + } + }, [hasBarChart, data, activeFunc, funcs, render, thread.op]); + return (
@@ -100,9 +138,9 @@ function VisualizationPanel() { {hasHistogram ? ( <> - +
- + ) : null} + {hasBarChart ? ( + <> + + +
+ + +
+
+ + ) : null} diff --git a/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx b/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx new file mode 100644 index 000000000000..5c610297fe49 --- /dev/null +++ b/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx @@ -0,0 +1,78 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import * as React from "react"; + +interface BarChartProps { + data: { x: string; y: number }[]; + labels: { + x: string; + y: string; + }; +} + +function BarChart({ data, labels }: BarChartProps) { + const ref = React.useRef(null); + + React.useEffect(() => { + if (!ref.current) { + return; + } + + const plot = Plot.plot({ + style: { + fontSize: "12px", + }, + width: 480, + marginBottom: 80, + y: { + grid: true, + label: labels.y, + tickFormat: (value) => d3.format(".2s")(value), + ticks: 8, + }, + x: { + label: labels.x, + labelAnchor: "right", + labelArrow: "right", + tickSize: 0, + tickRotate: -45, + type: "band", + }, + color: { + scheme: "Tableau10", + type: "ordinal", + }, + marks: [Plot.barY(data, { x: "x", y: "y", fill: "x" })], + }); + + ref.current.append(plot); + + return () => { + plot.remove(); + }; + }, [data, labels]); + + return data.every((d) => d.y === 0) ? ( +
+ + + + No data to display +
+ ) : ( +
+ ); +} + +export default BarChart; diff --git a/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx b/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx new file mode 100644 index 000000000000..87917fc93a55 --- /dev/null +++ b/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx @@ -0,0 +1,107 @@ +import { Label, Select } from "radix-ui"; +import { useAtom } from "jotai"; + +import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; +import { useTraceContext } from "@/hooks/trace"; +import { funcAtom } from "@/state/func"; +import { threadAtom } from "@/state/thread"; + +function BarChartParameters() { + const { funcs } = useTraceContext(); + const [activeFunc, setActiveFunc] = useAtom(funcAtom); + const [thread, setThread] = useAtom(threadAtom); + + return ( +
+
+ + Selected Func + + { + setActiveFunc(value); + }} + > + + + + + + + + + + + {Object.keys(funcs).map((func) => ( + + + {func} + + + ))} + + + +
+
+ + Operation + + { + setThread({ ...thread, op: value as "Load" | "Store" }); + }} + > + + + + + + + + + + + + Store + + + Load + + + + +
+
+ ); +} + +export default BarChartParameters; diff --git a/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx b/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx index c66fc8bced32..11fd3805fd4f 100644 --- a/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx +++ b/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx @@ -7,7 +7,7 @@ import { funcAtom } from "@/state/func"; import { histogramAtom, type HistogramScale } from "@/state/histogram"; import { type NormalizationMode, renderAtom } from "@/state/render"; -function HistogramSelect() { +function HistogramParameters() { const { funcs } = useTraceContext(); const [activeFunc, setActiveFunc] = useAtom(funcAtom); const [histogram, setHistogram] = useAtom(histogramAtom); @@ -28,26 +28,13 @@ function HistogramSelect() { > - + - - - + {func} @@ -172,4 +159,4 @@ function HistogramSelect() { ); } -export default HistogramSelect; +export default HistogramParameters; diff --git a/apps/halidoscope/src/components/controls/inf/InfControls.tsx b/apps/halidoscope/src/components/controls/inf/InfControls.tsx index 9d1b2edb6cf9..ea0587cfd16a 100644 --- a/apps/halidoscope/src/components/controls/inf/InfControls.tsx +++ b/apps/halidoscope/src/components/controls/inf/InfControls.tsx @@ -27,7 +27,7 @@ function InfControls() {
{inf.active ? ( -
+
Highlight NaN Values
{nan.active ? ( -
+
({ + id: 0, + op: "Store", +}); diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts index df6936384e0f..85e13229da01 100644 --- a/apps/halidoscope/src/types/index.ts +++ b/apps/halidoscope/src/types/index.ts @@ -27,6 +27,7 @@ export interface FuncMeta extends Record { produce_ranges: IndexRange[]; consume_ranges: IndexRange[]; thread_count: number; + thread_ids: number[]; } /** Top-level payload returned by `open_trace`. Mirrors the Rust `TraceMeta`. */ diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index 48d9d5ad35a3..d86a9a73d89b 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -1,6 +1,7 @@ import { invoke } from "@tauri-apps/api/core"; import type { NormalizationMode } from "@/state/render"; +import type { ThreadOpMode } from "@/state/thread"; import type { TraceMeta } from "@/types"; export interface RenderResult { @@ -158,3 +159,34 @@ export async function renderInf( histogram: null, }; } + +function splitPixelsAndThreadStoreLoadCounts( + buffer: ArrayBuffer, + width: number, + height: number, +): RenderResult { + const pixelByteLength = width * height * 4; + + return { + pixels: new Uint8ClampedArray(buffer, 0, pixelByteLength), + histogram: new Uint32Array(buffer, pixelByteLength), + }; +} + +export async function renderThread( + func: string, + globalIndex: number, + normalizationMode: NormalizationMode, + threadOpMode: ThreadOpMode, + width: number, + height: number, +): Promise { + const buffer = await invoke("render_thread", { + func, + globalIndex, + normalizationMode, + opMode: threadOpMode, + }); + + return splitPixelsAndThreadStoreLoadCounts(buffer, width, height); +} From 967e4b058a7d157e6c26000b30d39d43b99a230e Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Wed, 22 Jul 2026 14:48:09 -0700 Subject: [PATCH 28/67] Dramatically improve rendering performancefor Load Frequency render mode. --- apps/halidoscope/src-tauri/src/commands.rs | 61 ++++- apps/halidoscope/src-tauri/src/render.rs | 83 +++--- apps/halidoscope/src/App.tsx | 18 +- .../src/components/canvas/FuncNode.tsx | 149 +++++------ .../controls/VisualizationPanel.tsx | 230 +++++++++-------- .../controls/bar-chart/BarChart.tsx | 6 +- .../controls/histogram/Histogram.tsx | 7 +- .../histogram/HistogramParameters.tsx | 9 +- apps/halidoscope/src/hooks/trace.ts | 11 +- apps/halidoscope/src/state/histogram.ts | 11 - apps/halidoscope/src/state/tabularData.ts | 11 + apps/halidoscope/src/state/thread.ts | 2 +- apps/halidoscope/src/types/index.ts | 16 +- apps/halidoscope/src/utils/api.ts | 240 +++++++++++------- 14 files changed, 491 insertions(+), 363 deletions(-) delete mode 100644 apps/halidoscope/src/state/histogram.ts create mode 100644 apps/halidoscope/src/state/tabularData.ts diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index b2be3aba8844..e09ce8968384 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -51,20 +51,28 @@ pub struct FuncMeta { pub thread_ids: Vec, } +#[derive(Debug, Clone, Serialize)] +pub struct StatsMeta { + global_max_store_count: u32, + global_max_load_count: u32, + global_max_redundant_store_count: u32, + global_max_reuse_distance: u64, +} + /// Top-level payload returned by `open_trace`. #[derive(Debug, Clone, Serialize)] pub struct TraceMeta { pub funcs: Vec, pub total_packets: u32, pub dag_edges: BTreeMap>, - pub global_max_reuse_distance: u64, + pub stats: StatsMeta, } impl TraceMeta { - /// Derives the frontend contract from a parsed trace. Funcs with no usable coordinate extent - /// are still listed (with zero dimensions) so the UI can surface them; the renderer simply - /// produces nothing for them. pub fn from_trace(trace: &Trace) -> Self { + let mut global_max_store_count = 0u32; + let mut global_max_load_count = 0u32; + let mut global_max_redundant_store_count = 0u32; let mut global_max_reuse_distance = 0u64; let funcs = trace @@ -79,6 +87,11 @@ impl TraceMeta { let stores = trace.func_store_indices(name); let num_stores = stores.map(<[usize]>::len).unwrap_or(0) as u32; + global_max_store_count = stats.max_store_count.max(global_max_store_count); + global_max_load_count = stats.max_load_count.max(global_max_load_count); + global_max_redundant_store_count = stats + .max_redundant_store_count + .max(global_max_redundant_store_count); global_max_reuse_distance = stats.max_reuse_distance.max(global_max_reuse_distance); FuncMeta { @@ -139,7 +152,12 @@ impl TraceMeta { funcs, total_packets: trace.packets.len() as u32, dag_edges, - global_max_reuse_distance, + stats: StatsMeta { + global_max_store_count, + global_max_load_count, + global_max_redundant_store_count, + global_max_reuse_distance, + }, } } } @@ -168,7 +186,7 @@ pub struct AppState { inner: Mutex>, } -/// Appends `histogram`'s bins as little-endian `u32`s directly after `pixels`, so a single +/// Appends histogram data as little-endian `u32`s directly after `pixels`, so a single /// `Response` carries both. The frontend already knows the pixel-buffer length ahead of time /// (`width * height * 4`), so no length prefix is needed to split the two back apart. fn pack_pixels_and_histogram(mut pixels: Vec, histogram: Vec) -> Vec { @@ -176,6 +194,7 @@ fn pack_pixels_and_histogram(mut pixels: Vec, histogram: Vec) -> Vec, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -314,7 +335,11 @@ pub fn render_store_frequency( renderer.seek(trace, store_indices, k); let pixels = renderer.to_rgba(normalization_mode); - let histogram = renderer.to_histogram(normalization_mode); + let histogram = if include_tabular_data { + renderer.to_tabular_data(normalization_mode) + } else { + Vec::new() + }; Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) } @@ -324,6 +349,7 @@ pub fn render_load_frequency( func: String, global_index: u32, normalization_mode: NormalizationMode, + include_tabular_data: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -348,7 +374,12 @@ pub fn render_load_frequency( renderer.seek(trace, load_indices, k); let pixels = renderer.to_rgba(normalization_mode); - let histogram = renderer.to_histogram(normalization_mode); + let histogram = if include_tabular_data { + renderer.to_tabular_data(normalization_mode) + } else { + Vec::new() + }; + Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) } @@ -360,6 +391,7 @@ pub fn render_redundant_stores( func: String, global_index: u32, normalization_mode: NormalizationMode, + include_tabular_data: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -382,7 +414,11 @@ pub fn render_redundant_stores( renderer.seek(trace, store_indices, k); let pixels = renderer.to_rgba(normalization_mode); - let histogram = renderer.to_histogram(normalization_mode); + let histogram = if include_tabular_data { + renderer.to_tabular_data(normalization_mode) + } else { + Vec::new() + }; Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) } @@ -394,6 +430,7 @@ pub fn render_reuse_distance( func: String, global_index: u32, normalization_mode: NormalizationMode, + include_tabular_data: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -420,7 +457,11 @@ pub fn render_reuse_distance( renderer.seek(trace, store_indices, load_indices, store_k, load_k); let pixels = renderer.to_rgba(normalization_mode); - let histogram = renderer.to_histogram(normalization_mode); + let histogram = if include_tabular_data { + renderer.to_tabular_data(normalization_mode) + } else { + Vec::new() + }; Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) } diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index 5eb54c7731e9..b2d39bc994e2 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -309,18 +309,30 @@ impl StoreFrequencyState { } } - pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + pub fn to_tabular_data(&self, normalization_mode: NormalizationMode) -> Vec { let max = match normalization_mode { NormalizationMode::AcrossFuncs => self.global_max_store_count, NormalizationMode::PerFunc => self.local_max_store_count, }; - let mut hist = vec![0u32; max as usize + 1]; + + let exceeds_max_bins = max > 64; + // Pre-allocate tabular_data, capping to 64 bins. + let mut tabular_data = vec![ + 0u32; + if exceeds_max_bins { + 64 + } else { + max as usize + 1 + } + ]; for &c in &self.counts { - hist[c.clamp(0, max) as usize] += 1; + let bucket = if exceeds_max_bins { c * 63 / max } else { c }; + + tabular_data[bucket.clamp(0, 63) as usize] += 1; } - hist + tabular_data } } @@ -435,18 +447,30 @@ impl LoadFrequencyState { ); } - pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + pub fn to_tabular_data(&self, normalization_mode: NormalizationMode) -> Vec { let max = match normalization_mode { NormalizationMode::AcrossFuncs => self.global_max_load_count, NormalizationMode::PerFunc => self.local_max_load_count, }; - let mut hist = vec![0u32; max as usize + 1]; + + let exceeds_max_bins = max > 64; + // Pre-allocate tabular_data, capping to 64 bins. + let mut tabular_data = vec![ + 0u32; + if exceeds_max_bins { + 64 + } else { + max as usize + 1 + } + ]; for &c in &self.counts { - hist[c.clamp(0, max) as usize] += 1; + let bucket = if exceeds_max_bins { c * 63 / max } else { c }; + + tabular_data[bucket.clamp(0, 63) as usize] += 1; } - hist + tabular_data } } @@ -581,16 +605,30 @@ impl RedundantState { ); } - pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + pub fn to_tabular_data(&self, normalization_mode: NormalizationMode) -> Vec { let max = match normalization_mode { NormalizationMode::AcrossFuncs => self.global_max_redundant_store_count, NormalizationMode::PerFunc => self.local_max_redundant_store_count, }; - let mut hist = vec![0u32; max as usize + 1]; + + let exceeds_max_bins = max > 64; + // Pre-allocate tabular_data, capping to 64 bins. + let mut tabular_data = vec![ + 0u32; + if exceeds_max_bins { + 64 + } else { + max as usize + 1 + } + ]; + for &c in &self.redundant_store_counts { - hist[c.clamp(0, max) as usize] += 1; + let bucket = if exceeds_max_bins { c * 63 / max } else { c }; + + tabular_data[bucket.clamp(0, 63) as usize] += 1; } - hist + + tabular_data } } @@ -854,23 +892,23 @@ impl ReuseDistanceState { out } - pub fn to_histogram(&self, normalization_mode: NormalizationMode) -> Vec { + pub fn to_tabular_data(&self, normalization_mode: NormalizationMode) -> Vec { let max = match normalization_mode { NormalizationMode::AcrossFuncs => self.global_max_reuse_distance, NormalizationMode::PerFunc => self.local_max_reuse_distance, }; - let mut hist = vec![0u32; 64]; + let mut tabular_data = vec![0u32; 64]; if max > 0 { for &dist in &self.max_reuse_distance { if dist > 0 { let bucket = ((dist as f64 / max as f64) * 63.0) as usize; - hist[bucket.min(63)] += 1; + tabular_data[bucket.min(63)] += 1; } } } - hist + tabular_data } /// Returns the per-pixel maximum reuse distance at the current seek position, in row-major @@ -1132,7 +1170,6 @@ impl Renderer for InfState { pub enum ThreadOpMode { Store, Load, - All, } pub struct ThreadState { @@ -1271,26 +1308,14 @@ impl ThreadState { && (li >= load_slice.len() || store_slice[si] < load_slice[li]); match (&op_mode, next_is_store) { - // op_mode is Store and the next packet is a store. (ThreadOpMode::Store, true) => { self.apply_store(&trace.packets[store_slice[si]]); si += 1; } - // op_mode is Load and the next packet is a load. (ThreadOpMode::Load, false) => { self.apply_load(&trace.packets[load_slice[li]]); li += 1; } - // op_mode is All and the next packet is a store. - (ThreadOpMode::All, true) => { - self.apply_store(&trace.packets[store_slice[si]]); - si += 1; - } - // op_mode is All and the next packet is a load. - (ThreadOpMode::All, false) => { - self.apply_load(&trace.packets[load_slice[li]]); - li += 1; - } (_, true) => { // Increment si even if we don't apply the store, to keep the merge moving forward. si += 1; diff --git a/apps/halidoscope/src/App.tsx b/apps/halidoscope/src/App.tsx index ead0b8bdc1d4..ddb80c089a02 100644 --- a/apps/halidoscope/src/App.tsx +++ b/apps/halidoscope/src/App.tsx @@ -5,18 +5,22 @@ import * as React from "react"; import Tracer from "@/components/views/tracer/Tracer"; import { TraceContextProvider } from "@/hooks/trace"; -import type { FuncMeta } from "@/types"; +import { funcAtom } from "@/state/func"; +import type { FuncMeta, StatsMeta } from "@/types"; import { openTrace } from "@/utils/api"; import "./App.css"; -import { funcAtom } from "./state/func"; function App() { const [funcs, setFuncs] = React.useState>({}); const [dagEdges, setDagEdges] = React.useState>({}); const [packetCount, setPacketCount] = React.useState(0); - const [globalMaxReuseDistance, setGlobalMaxReuseDistance] = - React.useState(0); + const [stats, setStats] = React.useState({ + global_max_store_count: 0, + global_max_load_count: 0, + global_max_redundant_store_count: 0, + global_max_reuse_distance: 0, + }); const setActiveFunc = useSetAtom(funcAtom); @@ -34,7 +38,7 @@ function App() { : `${await invoke("get_cwd")}/${tracePath}`; try { - const { funcs, total_packets, dag_edges, global_max_reuse_distance } = + const { funcs, total_packets, dag_edges, stats } = await openTrace(resolved); const byName: Record = {}; @@ -45,7 +49,7 @@ function App() { setFuncs(byName); setDagEdges(dag_edges); setPacketCount(total_packets); - setGlobalMaxReuseDistance(global_max_reuse_distance); + setStats(stats); setActiveFunc(funcs[0]?.name ?? ""); } catch (err) { console.error("Error loading trace from CLI: ", err); @@ -61,7 +65,7 @@ function App() { funcs, dagEdges, packetCount, - globalMaxReuseDistance, + stats, }} >
diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 0aa1be350ccc..01e3be3cdd49 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -17,12 +17,12 @@ import * as React from "react"; import HandleCircle from "@/components/canvas/HandleCircle"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; -import { histogramAtom } from "@/state/histogram"; import { infAtom } from "@/state/inf"; import { livenessAtom } from "@/state/liveness"; import { packetAtom } from "@/state/packet"; import { nanAtom } from "@/state/nan"; import { renderAtom } from "@/state/render"; +import { tabularDataAtom } from "@/state/tabularData"; import { threadAtom } from "@/state/thread"; import type { FuncMeta } from "@/types"; import { @@ -32,10 +32,11 @@ import { renderLoadFrequency, renderRedundantStores, renderReuseDistance, - type RenderResult, renderNaN, renderInf, renderThread, + type RenderFuncParams, + type RenderFuncResponse, } from "@/utils/api"; import { isFuncBufferLive, isEdgeLive } from "@/utils/liveness"; @@ -50,7 +51,7 @@ function FuncNode({ data }: NodeProps>) { const packetIndex = useAtomValue(packetAtom); const render = useAtomValue(renderAtom); const activeFunc = useAtomValue(funcAtom); - const setHistogramData = useSetAtom(histogramAtom); + const setTabularData = useSetAtom(tabularDataAtom); const nan = useAtomValue(nanAtom); const inf = useAtomValue(infAtom); const thread = useAtomValue(threadAtom); @@ -58,6 +59,7 @@ function FuncNode({ data }: NodeProps>) { const nodes = useNodes(); const edges = useEdges(); + const active = activeFunc === name; const bufferLive = React.useMemo( () => liveness.active && @@ -65,7 +67,6 @@ function FuncNode({ data }: NodeProps>) { isFuncBufferLive(data, packetIndex), [liveness, data, packetIndex], ); - const producing = React.useMemo( () => liveness.active && @@ -77,7 +78,6 @@ function FuncNode({ data }: NodeProps>) { ), [liveness, edges, funcs, name, packetIndex], ); - const consuming = React.useMemo( () => liveness.active && @@ -98,18 +98,18 @@ function FuncNode({ data }: NodeProps>) { () => getOutgoers({ id: name }, nodes, edges).length, [name, nodes, edges], ); + const { zoom } = useViewport(); - // Latest playhead position requested, and whether a render loop is draining. - // Together these coalesce rapid scrub updates: while a frame is in flight, - // newer indices just overwrite `latestIndexRef`, and the loop renders only - // the most recent one rather than every intermediate position. + // Track the playhead position as a ref to avoid re-rendering on every scrub. const latestIndexRef = React.useRef(packetIndex); + // Track whether an active render is in progress. const renderingRef = React.useRef(false); React.useEffect(() => { latestIndexRef.current = packetIndex; + // Return early if we're actively writing a tensor. if (renderingRef.current) { return; } @@ -119,78 +119,59 @@ function FuncNode({ data }: NodeProps>) { while (true) { const target = latestIndexRef.current; - let result: RenderResult; + let result: RenderFuncResponse; + const params: RenderFuncParams = { + func: name, + globalIndex: target, + normalizationMode: render.normalizationMode, + width, + height, + includeTabularData: active, + }; switch (render.renderMode) { case "Grayscale": - result = await renderGrayscale( - name, - target, - render.normalizationMode, - ); + result = await renderGrayscale(params); break; case "RGB": - result = await renderRgb(name, target, render.normalizationMode); + result = await renderRgb(params); break; case "Store Frequency": - result = await renderStoreFrequency( - name, - target, - render.normalizationMode, - width, - height, - ); + result = await renderStoreFrequency(params); break; case "Load Frequency": - result = await renderLoadFrequency( - name, - target, - render.normalizationMode, - width, - height, - ); + result = await renderLoadFrequency(params); break; case "Redundant Stores": - result = await renderRedundantStores( - name, - target, - render.normalizationMode, - width, - height, - ); + result = await renderRedundantStores(params); break; case "Reuse Distance": - result = await renderReuseDistance( - name, - target, - render.normalizationMode, - width, - height, - ); + result = await renderReuseDistance(params); break; case "Thread Coverage": - result = await renderThread( - name, - target, - render.normalizationMode, - thread.op, - width, - height, - ); + result = await renderThread({ + ...params, + threadOpMode: thread.op, + }); break; } const ctx = canvasRef.current?.getContext("2d"); - - // Update this Func's canvas with new pixel data. if (ctx) { - ctx.putImageData(new ImageData(result.pixels, width, height), 0, 0); + ctx.putImageData( + new ImageData(result.tensorData, width, height), + 0, + 0, + ); } // Update the histogram data for the currently active Func. - if (name === activeFunc) { - setHistogramData((prev) => ({ ...prev, data: result.histogram })); + if (active) { + setTabularData((prev) => ({ + ...prev, + tabularData: result.tabularData, + })); } if (latestIndexRef.current === target) { @@ -206,24 +187,21 @@ function FuncNode({ data }: NodeProps>) { draw(); }, [ + active, packetIndex, name, width, height, render, activeFunc, - setHistogramData, + setTabularData, thread.op, ]); React.useEffect(() => { - if (!nan.active) { - return; - } - latestIndexRef.current = packetIndex; - if (renderingRef.current) { + if (!nan.active || renderingRef.current) { return; } @@ -232,16 +210,23 @@ function FuncNode({ data }: NodeProps>) { while (true) { const target = latestIndexRef.current; - const result = await renderNaN( - name, - packetIndex, - render.normalizationMode, - ); + const result = await renderNaN({ + func: name, + globalIndex: packetIndex, + normalizationMode: render.normalizationMode, + width, + height, + includeTabularData: false, + }); const ctx = nanOverlayRef.current?.getContext("2d"); if (ctx) { - ctx.putImageData(new ImageData(result.pixels, width, height), 0, 0); + ctx.putImageData( + new ImageData(result.tensorData, width, height), + 0, + 0, + ); } if (latestIndexRef.current === target) { @@ -259,13 +244,9 @@ function FuncNode({ data }: NodeProps>) { }, [nan.active, name, packetIndex, render.normalizationMode, width, height]); React.useEffect(() => { - if (!inf.active) { - return; - } - latestIndexRef.current = packetIndex; - if (renderingRef.current) { + if (!inf.active || renderingRef.current) { return; } @@ -274,16 +255,22 @@ function FuncNode({ data }: NodeProps>) { while (true) { const target = latestIndexRef.current; - const result = await renderInf( - name, - packetIndex, - render.normalizationMode, - ); + const result = await renderInf({ + func: name, + globalIndex: packetIndex, + normalizationMode: render.normalizationMode, + width, + height, + includeTabularData: false, + }); const ctx = infOverlayRef.current?.getContext("2d"); - if (ctx) { - ctx.putImageData(new ImageData(result.pixels, width, height), 0, 0); + ctx.putImageData( + new ImageData(result.tensorData, width, height), + 0, + 0, + ); } if (latestIndexRef.current === target) { diff --git a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx index 6a8f90f0528b..0b5534a9216b 100644 --- a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -13,19 +13,10 @@ import PlaybackRate from "@/components/controls/playback/PlaybackRate"; import RenderMode from "@/components/controls/render/RenderMode"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; -import { histogramAtom } from "@/state/histogram"; import { type RenderMode as RM, renderAtom } from "@/state/render"; +import { tabularDataAtom } from "@/state/tabularData"; import { threadAtom } from "@/state/thread"; -const HISTOGRAM_RENDER_MODES = new Set([ - "Store Frequency", - "Load Frequency", - "Redundant Stores", - "Reuse Distance", -]); - -const BAR_CHART_RENDER_MODES = new Set(["Thread Coverage"]); - const RENDER_MODE_TO_LABEL: Record = { Grayscale: "", RGB: "", @@ -36,134 +27,155 @@ const RENDER_MODE_TO_LABEL: Record = { "Thread Coverage": "Thread ID", }; +interface HistogramData { + type: "Histogram"; + data: { x1: number; x2: number; y: number }[]; + domain: [number, number]; +} + +interface BarChartData { + type: "Bar Chart"; + data: { x: string; y: number }[]; + domain: string[]; +} + +interface NoChartData { + type: "No Chart"; + data: number[]; + domain: [number, number]; +} + +type ChartData = HistogramData | BarChartData | NoChartData; + function VisualizationPanel() { - const { funcs, globalMaxReuseDistance } = useTraceContext(); + const { funcs, stats } = useTraceContext(); const render = useAtomValue(renderAtom); const activeFunc = useAtomValue(funcAtom); - const { data, scale } = useAtomValue(histogramAtom); + const { tabularData, scale } = useAtomValue(tabularDataAtom); const thread = useAtomValue(threadAtom); + const min = scale === "log" ? 1 : 0; - const hasHistogram = - HISTOGRAM_RENDER_MODES.has(render.renderMode) && - activeFunc && - funcs[activeFunc] && - data !== null; - const domainMin = scale === "log" ? 1 : 0; - - const { data: histogramData, domain: histogramDomain } = React.useMemo((): { - data: { x1: number; x2: number; y: number }[]; - domain: [number, number]; - } => { - if (!hasHistogram || !data) { - return { data: [], domain: [domainMin, 1] }; - } + const createHistogramData = React.useCallback( + (histogramData: Uint32Array, max: number): HistogramData => { + const buckets = histogramData.length; + + return { + type: "Histogram", + data: new Array(buckets).fill(0).map((_, i) => ({ + x1: max > 64 ? Math.round((i / 64) * max) : i, + x2: max > 64 ? Math.round(((i + 1) / 64) * max) : i + 1, + y: histogramData?.[i] ?? 0, + })), + domain: [min, max + 1], + }; + }, + [min], + ); + const { type, data, domain } = React.useMemo((): ChartData => { switch (render.renderMode) { - case "Store Frequency": - case "Load Frequency": - case "Redundant Stores": - return { - data: Array.from(data).map((y, i) => ({ - x1: i, - x2: i + 1, - y, - })), - domain: [domainMin, data.length], - }; - case "Reuse Distance": { - // For Reuse Distance, scale x values based on the normalization mode. - const domainMax = + case "Store Frequency": { + const max = render.normalizationMode === "Per Func" - ? funcs[activeFunc].max_reuse_distance - : globalMaxReuseDistance; + ? funcs[activeFunc].max_store_count + : stats.global_max_store_count; - return { - data: Array.from(data).map((y, i) => ({ - x1: Math.round((i / 64) * domainMax), - x2: Math.round(((i + 1) / 64) * domainMax), - y, - })), - domain: [domainMin, domainMax], - }; + return createHistogramData(tabularData ?? new Uint32Array(), max); } - default: - return { data: [], domain: [domainMin, 1] }; - } - }, [ - hasHistogram, - data, - activeFunc, - funcs, - render, - domainMin, - globalMaxReuseDistance, - ]); + case "Load Frequency": { + const max = + render.normalizationMode === "Per Func" + ? funcs[activeFunc].max_load_count + : stats.global_max_load_count; - const hasBarChart = - BAR_CHART_RENDER_MODES.has(render.renderMode) && - activeFunc && - funcs[activeFunc] && - data !== null; - - const { data: barChartData } = React.useMemo((): { - data: { x: string; y: number }[]; - } => { - if (!hasBarChart || !data) { - return { data: [] }; - } + return createHistogramData(tabularData ?? new Uint32Array(), max); + } + case "Redundant Stores": { + const max = + render.normalizationMode === "Per Func" + ? funcs[activeFunc].max_redundant_store_count + : stats.global_max_redundant_store_count; - switch (render.renderMode) { + return createHistogramData(tabularData ?? new Uint32Array(), max); + } + case "Reuse Distance": { + const max = + render.normalizationMode === "Per Func" + ? funcs[activeFunc].max_reuse_distance + : stats.global_max_reuse_distance; + + return createHistogramData(tabularData ?? new Uint32Array(), max); + } case "Thread Coverage": { const threadIds = funcs[activeFunc].thread_ids; - const storeCounts = data.slice(0, threadIds.length); - const loadCounts = data.slice(threadIds.length, threadIds.length * 2); + const storeCounts = tabularData?.slice(0, threadIds.length) ?? []; + const loadCounts = + tabularData?.slice(threadIds.length, threadIds.length * 2) ?? []; return { + type: "Bar Chart", data: threadIds.map((threadId, i) => ({ x: `${threadId}`, y: thread.op === "Store" ? storeCounts[i] : loadCounts[i], })), + domain: threadIds.map((tId) => `${tId}`), }; } - default: - return { data: [] }; + default: { + return { type: "No Chart", data: [], domain: [-1, -1] }; + } + } + }, [ + render, + tabularData, + funcs, + stats, + activeFunc, + thread.op, + createHistogramData, + ]); + + const renderChart = React.useCallback(() => { + switch (type) { + case "Histogram": + return ( + <> + + + + ); + case "Bar Chart": + return ( + <> + + + + ); + case "No Chart": + return null; } - }, [hasBarChart, data, activeFunc, funcs, render, thread.op]); + }, [type, data, domain, render.renderMode, thread.op]); return (
- {hasHistogram ? ( - <> - - -
- - -
-
- - ) : null} - {hasBarChart ? ( - <> - - -
- - -
-
- - ) : null} + {renderChart()} diff --git a/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx b/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx index 5c610297fe49..38bd0b003dc6 100644 --- a/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx +++ b/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx @@ -4,13 +4,14 @@ import * as React from "react"; interface BarChartProps { data: { x: string; y: number }[]; + domain: string[]; labels: { x: string; y: string; }; } -function BarChart({ data, labels }: BarChartProps) { +function BarChart({ data, domain, labels }: BarChartProps) { const ref = React.useRef(null); React.useEffect(() => { @@ -31,6 +32,7 @@ function BarChart({ data, labels }: BarChartProps) { ticks: 8, }, x: { + domain, label: labels.x, labelAnchor: "right", labelArrow: "right", @@ -50,7 +52,7 @@ function BarChart({ data, labels }: BarChartProps) { return () => { plot.remove(); }; - }, [data, labels]); + }, [data, labels, domain]); return data.every((d) => d.y === 0) ? (
diff --git a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx index 2d2ec7186c75..7e64f1aa8a8e 100644 --- a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx +++ b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx @@ -3,19 +3,20 @@ import * as d3 from "d3"; import { useAtomValue } from "jotai"; import * as React from "react"; -import { histogramAtom } from "@/state/histogram"; +import { tabularDataAtom } from "@/state/tabularData"; interface HistogramProps { data: { x1: number; x2: number; y: number }[]; domain: [number, number]; labels: { x: string; + y: string; }; } function Histogram({ data, domain, labels }: HistogramProps) { const ref = React.useRef(null); - const { scale } = useAtomValue(histogramAtom); + const { scale } = useAtomValue(tabularDataAtom); // Build the data for the bottom colorbar. const colorbar = React.useMemo(() => { @@ -41,7 +42,7 @@ function Histogram({ data, domain, labels }: HistogramProps) { marginBottom: 60, y: { grid: true, - label: "Coordinate Count", + label: labels.y, tickFormat: (value) => d3.format(".2s")(value), ticks: 8, }, diff --git a/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx b/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx index 11fd3805fd4f..73f67f1d9677 100644 --- a/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx +++ b/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx @@ -4,13 +4,13 @@ import { useAtom } from "jotai"; import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; -import { histogramAtom, type HistogramScale } from "@/state/histogram"; +import { tabularDataAtom, type Scale } from "@/state/tabularData"; import { type NormalizationMode, renderAtom } from "@/state/render"; function HistogramParameters() { const { funcs } = useTraceContext(); const [activeFunc, setActiveFunc] = useAtom(funcAtom); - const [histogram, setHistogram] = useAtom(histogramAtom); + const [tabularData, setTabularData] = useAtom(tabularDataAtom); const [render, setRender] = useAtom(renderAtom); return ( @@ -23,7 +23,6 @@ function HistogramParameters() { value={activeFunc} onValueChange={(value) => { setActiveFunc(value); - setHistogram({ ...histogram, data: null }); }} > - setHistogram({ ...histogram, scale: value as HistogramScale }) + setTabularData({ ...tabularData, scale: value as Scale }) } > ; dagEdges: Record; packetCount: number; - globalMaxReuseDistance: number; + stats: StatsMeta; }>({ funcs: {}, dagEdges: {}, packetCount: 0, - globalMaxReuseDistance: 0, + stats: { + global_max_store_count: 0, + global_max_load_count: 0, + global_max_redundant_store_count: 0, + global_max_reuse_distance: 0, + }, }); export const TraceContextProvider = TraceContext.Provider; diff --git a/apps/halidoscope/src/state/histogram.ts b/apps/halidoscope/src/state/histogram.ts deleted file mode 100644 index a6c3f1f2a1df..000000000000 --- a/apps/halidoscope/src/state/histogram.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { atom } from "jotai"; - -export type HistogramScale = "linear" | "log"; - -export const histogramAtom = atom<{ - data: Uint32Array | null; - scale: HistogramScale; -}>({ - data: null, - scale: "linear", -}); diff --git a/apps/halidoscope/src/state/tabularData.ts b/apps/halidoscope/src/state/tabularData.ts new file mode 100644 index 000000000000..f8b21997a07c --- /dev/null +++ b/apps/halidoscope/src/state/tabularData.ts @@ -0,0 +1,11 @@ +import { atom } from "jotai"; + +export type Scale = "linear" | "log"; + +export const tabularDataAtom = atom<{ + tabularData: Uint32Array | null; + scale: Scale; +}>({ + tabularData: null, + scale: "linear", +}); diff --git a/apps/halidoscope/src/state/thread.ts b/apps/halidoscope/src/state/thread.ts index 92d3f2b4020f..0a7747964b97 100644 --- a/apps/halidoscope/src/state/thread.ts +++ b/apps/halidoscope/src/state/thread.ts @@ -1,6 +1,6 @@ import { atom } from "jotai"; -export type ThreadOpMode = "Store" | "Load" | "All"; +export type ThreadOpMode = "Store" | "Load"; export const threadAtom = atom<{ id: number; op: ThreadOpMode }>({ id: 0, op: "Store", diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts index 85e13229da01..35f6523dd558 100644 --- a/apps/halidoscope/src/types/index.ts +++ b/apps/halidoscope/src/types/index.ts @@ -1,14 +1,8 @@ -/** A packet-index interval `[start, end]`. Mirrors the Rust `IndexRange`. */ export interface IndexRange { start: number; end: number; } -/** - * Per-Func metadata returned by the `open_trace` command. Mirrors the Rust - * `FuncMeta`. Carries everything the UI needs to size canvases, bound the - * scrub timeline, and populate the inspector panel. - */ export interface FuncMeta extends Record { name: string; width: number; @@ -30,12 +24,18 @@ export interface FuncMeta extends Record { thread_ids: number[]; } -/** Top-level payload returned by `open_trace`. Mirrors the Rust `TraceMeta`. */ +export interface StatsMeta { + global_max_store_count: number; + global_max_load_count: number; + global_max_redundant_store_count: number; + global_max_reuse_distance: number; +} + export interface TraceMeta { funcs: FuncMeta[]; total_packets: number; dag_edges: Record; - global_max_reuse_distance: number; + stats: StatsMeta; } export type NodeTypes = "funcNode"; diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index d86a9a73d89b..854f283b129b 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -4,20 +4,29 @@ import type { NormalizationMode } from "@/state/render"; import type { ThreadOpMode } from "@/state/thread"; import type { TraceMeta } from "@/types"; -export interface RenderResult { - pixels: Uint8ClampedArray; - histogram: Uint32Array | null; -} - export async function openTrace(path: string): Promise { return invoke("open_trace", { path }); } -export async function renderGrayscale( - func: string, - globalIndex: number, - normalizationMode: NormalizationMode, -): Promise { +export interface RenderFuncResponse { + tensorData: Uint8ClampedArray; + tabularData: Uint32Array | null; +} + +export interface RenderFuncParams { + func: string; + globalIndex: number; + normalizationMode: NormalizationMode; + width: number; + height: number; + includeTabularData: boolean; +} + +export async function renderGrayscale({ + func, + globalIndex, + normalizationMode, +}: RenderFuncParams): Promise { const buffer = await invoke("render_grayscale", { func, globalIndex, @@ -25,16 +34,16 @@ export async function renderGrayscale( }); return { - pixels: new Uint8ClampedArray(buffer), - histogram: null, + tensorData: new Uint8ClampedArray(buffer), + tabularData: null, }; } -export async function renderRgb( - func: string, - globalIndex: number, - normalizationMode: NormalizationMode, -): Promise { +export async function renderRgb({ + func, + globalIndex, + normalizationMode, +}: RenderFuncParams): Promise { const buffer = await invoke("render_rgb", { func, globalIndex, @@ -42,95 +51,141 @@ export async function renderRgb( }); return { - pixels: new Uint8ClampedArray(buffer), - histogram: null, + tensorData: new Uint8ClampedArray(buffer), + tabularData: null, }; } -// The backend appends the histogram's bins as little-endian u32s directly after the pixel -// bytes in a single response; split at the known pixel-buffer length to recover both. -function splitPixelsAndHistogram( - buffer: ArrayBuffer, - width: number, - height: number, -): RenderResult { +/** + * Split the ArrayBuffer returned by {@link RenderFuncResponse} into the tensor + * data and the (optionally returned) tabular data. We only include tabular + * data for the actively selected Func. + * + * @param buffer The buffer containing tensor data. + * @param width The width of the buffer. + * @param height The height of the buffer. + * @param includeTabularData A flag indicating whether of not to expect tabular + * data in the buffer payload. + * @returns A {@link RenderFuncResponse}. + */ +function splitTensorDataAndTabularData({ + buffer, + width, + height, + includeTabularData, +}: { + buffer: ArrayBuffer; + width: number; + height: number; + includeTabularData: boolean; +}): RenderFuncResponse { const pixelByteLength = width * height * 4; return { - pixels: new Uint8ClampedArray(buffer, 0, pixelByteLength), - histogram: new Uint32Array(buffer, pixelByteLength), + tensorData: new Uint8ClampedArray(buffer, 0, pixelByteLength), + tabularData: includeTabularData + ? new Uint32Array(buffer, pixelByteLength) + : null, }; } -export async function renderStoreFrequency( - func: string, - globalIndex: number, - normalizationMode: NormalizationMode, - width: number, - height: number, -): Promise { +export async function renderStoreFrequency({ + func, + globalIndex, + normalizationMode, + width, + height, + includeTabularData, +}: RenderFuncParams): Promise { const buffer = await invoke("render_store_frequency", { func, globalIndex, normalizationMode, + includeTabularData, }); - return splitPixelsAndHistogram(buffer, width, height); + return splitTensorDataAndTabularData({ + buffer, + width, + height, + includeTabularData, + }); } -export async function renderLoadFrequency( - func: string, - globalIndex: number, - normalizationMode: NormalizationMode, - width: number, - height: number, -): Promise { +export async function renderLoadFrequency({ + func, + globalIndex, + normalizationMode, + width, + height, + includeTabularData, +}: RenderFuncParams): Promise { const buffer = await invoke("render_load_frequency", { func, globalIndex, normalizationMode, + includeTabularData, }); - return splitPixelsAndHistogram(buffer, width, height); + return splitTensorDataAndTabularData({ + buffer, + width, + height, + includeTabularData, + }); } -export async function renderRedundantStores( - func: string, - globalIndex: number, - normalizationMode: NormalizationMode, - width: number, - height: number, -): Promise { +export async function renderRedundantStores({ + func, + globalIndex, + normalizationMode, + width, + height, + includeTabularData, +}: RenderFuncParams): Promise { const buffer = await invoke("render_redundant_stores", { func, globalIndex, normalizationMode, + includeTabularData, }); - return splitPixelsAndHistogram(buffer, width, height); + return splitTensorDataAndTabularData({ + buffer, + width, + height, + includeTabularData, + }); } -export async function renderReuseDistance( - func: string, - globalIndex: number, - normalizationMode: NormalizationMode, - width: number, - height: number, -): Promise { +export async function renderReuseDistance({ + func, + globalIndex, + normalizationMode, + width, + height, + includeTabularData, +}: RenderFuncParams): Promise { const buffer = await invoke("render_reuse_distance", { func, globalIndex, normalizationMode, + includeTabularData, }); - return splitPixelsAndHistogram(buffer, width, height); + return splitTensorDataAndTabularData({ + buffer, + width, + height, + includeTabularData, + }); } -export async function renderNaN( - func: string, - globalIndex: number, - normalizationMode: NormalizationMode, -): Promise { +export async function renderNaN({ + func, + globalIndex, + normalizationMode, +}: RenderFuncParams): Promise { const buffer = await invoke("render_nan", { func, globalIndex, @@ -138,16 +193,16 @@ export async function renderNaN( }); return { - pixels: new Uint8ClampedArray(buffer), - histogram: null, + tensorData: new Uint8ClampedArray(buffer), + tabularData: null, }; } -export async function renderInf( - func: string, - globalIndex: number, - normalizationMode: NormalizationMode, -): Promise { +export async function renderInf({ + func, + globalIndex, + normalizationMode, +}: RenderFuncParams): Promise { const buffer = await invoke("render_inf", { func, globalIndex, @@ -155,32 +210,24 @@ export async function renderInf( }); return { - pixels: new Uint8ClampedArray(buffer), - histogram: null, + tensorData: new Uint8ClampedArray(buffer), + tabularData: null, }; } -function splitPixelsAndThreadStoreLoadCounts( - buffer: ArrayBuffer, - width: number, - height: number, -): RenderResult { - const pixelByteLength = width * height * 4; - - return { - pixels: new Uint8ClampedArray(buffer, 0, pixelByteLength), - histogram: new Uint32Array(buffer, pixelByteLength), - }; +export interface RenderThreadFuncParams extends RenderFuncParams { + threadOpMode: ThreadOpMode; } -export async function renderThread( - func: string, - globalIndex: number, - normalizationMode: NormalizationMode, - threadOpMode: ThreadOpMode, - width: number, - height: number, -): Promise { +export async function renderThread({ + func, + globalIndex, + normalizationMode, + threadOpMode, + width, + height, + includeTabularData, +}: RenderThreadFuncParams): Promise { const buffer = await invoke("render_thread", { func, globalIndex, @@ -188,5 +235,10 @@ export async function renderThread( opMode: threadOpMode, }); - return splitPixelsAndThreadStoreLoadCounts(buffer, width, height); + return splitTensorDataAndTabularData({ + buffer, + width, + height, + includeTabularData, + }); } From 857a37716843dd74d81da64b58beb01d3de6ba41 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 23 Jul 2026 14:20:05 -0700 Subject: [PATCH 29/67] Add support for thread filtering and highlighting. --- apps/halidoscope/src-tauri/src/commands.rs | 22 ++- apps/halidoscope/src-tauri/src/render.rs | 45 ++++-- apps/halidoscope/src-tauri/src/trace.rs | 37 ++--- apps/halidoscope/src/App.tsx | 1 + .../src/components/canvas/FuncNode.tsx | 3 +- .../controls/VisualizationPanel.tsx | 72 ++++++--- .../controls/bar-chart/BarChart.tsx | 19 ++- .../controls/bar-chart/BarChartParameters.tsx | 143 ++++++++++++------ .../controls/histogram/Histogram.tsx | 1 + apps/halidoscope/src/hooks/trace.ts | 1 + apps/halidoscope/src/state/thread.ts | 5 +- apps/halidoscope/src/types/index.ts | 3 +- apps/halidoscope/src/utils/api.ts | 3 + 13 files changed, 230 insertions(+), 125 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index e09ce8968384..9e9259661d34 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -2,7 +2,7 @@ //! //! This module owns the types that cross the Tauri IPC boundary. -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::sync::Mutex; use serde::Serialize; @@ -48,7 +48,7 @@ pub struct FuncMeta { pub produce_ranges: Vec, pub consume_ranges: Vec, pub thread_count: u32, - pub thread_ids: Vec, + pub thread_ids: Vec, } #[derive(Debug, Clone, Serialize)] @@ -57,6 +57,7 @@ pub struct StatsMeta { global_max_load_count: u32, global_max_redundant_store_count: u32, global_max_reuse_distance: u64, + global_thread_ids: Vec, } /// Top-level payload returned by `open_trace`. @@ -74,6 +75,7 @@ impl TraceMeta { let mut global_max_load_count = 0u32; let mut global_max_redundant_store_count = 0u32; let mut global_max_reuse_distance = 0u64; + let mut global_thread_ids: BTreeSet = BTreeSet::new(); let funcs = trace .funcs @@ -94,6 +96,10 @@ impl TraceMeta { .max(global_max_redundant_store_count); global_max_reuse_distance = stats.max_reuse_distance.max(global_max_reuse_distance); + if let Some(thread_ids) = trace.func_thread_ids(name) { + global_thread_ids.extend(thread_ids); + } + FuncMeta { name: name.clone(), width, @@ -136,8 +142,8 @@ impl TraceMeta { .map_or(1, |ids| ids.len() as u32), thread_ids: trace .func_thread_ids(name) - .map(|ids| ids.iter().copied().collect()) - .unwrap_or_else(|| vec![0]), + .map(|ids| ids.into_iter().map(|x| x.to_string()).collect()) + .unwrap_or_else(|| vec![]), } }) .collect(); @@ -157,6 +163,10 @@ impl TraceMeta { global_max_load_count, global_max_redundant_store_count, global_max_reuse_distance, + global_thread_ids: global_thread_ids + .into_iter() + .map(|id| id.to_string()) + .collect(), }, } } @@ -527,8 +537,8 @@ pub fn render_inf( pub fn render_thread( func: String, global_index: u32, - normalization_mode: NormalizationMode, op_mode: ThreadOpMode, + thread_id: String, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -551,7 +561,7 @@ pub fn render_thread( let load_k = load_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, store_indices, load_indices, store_k, load_k, op_mode); - let pixels = renderer.to_rgba(normalization_mode); + let pixels = renderer.to_rgba(thread_id); let (store_counts, load_counts) = renderer.to_thread_counts(); Ok(Response::new(pack_pixels_and_thread_counts( pixels, diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index b2d39bc994e2..c98a612abc98 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -1176,6 +1176,7 @@ pub struct ThreadState { geom: FuncGeometry, thread_ids: Vec, thread_id_buffer: Vec, + global_thread_ids: Vec, store_counts: Vec, load_counts: Vec, applied_store_k: usize, @@ -1186,23 +1187,29 @@ pub struct ThreadState { impl ThreadState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; - // A missing entry means this Func was never realized inside a `BeginParallelTask` (i.e. - // it ran entirely serially) — every packet's `thread_id` defaults to 0 in that case. let thread_ids = trace .func_thread_ids(func) .map(|ids| ids.iter().map(|&id| id as i32).collect::>()) .unwrap_or_else(|| vec![0]); let n_threads = thread_ids.len(); - // `i32::MIN` marks a pixel no store/load has touched yet. Plain `0` would collide with a - // real thread ID, making untouched pixels indistinguishable from ones actually written by - // thread 0. - let thread_id_buffer = vec![i32::MIN; geom.width * geom.height]; + let global_thread_ids: Vec = trace + .thread_ids_by_func + .values() + .flatten() + .copied() + .collect::>() + .into_iter() + .collect(); + + // `-1` marks a pixel no store/load has touched yet. + let thread_id_buffer = vec![-1; geom.width * geom.height]; Some(Self { geom, thread_ids, thread_id_buffer, + global_thread_ids, store_counts: vec![0u32; n_threads], load_counts: vec![0u32; n_threads], applied_store_k: 0, @@ -1212,7 +1219,7 @@ impl ThreadState { } fn reset(&mut self) { - self.thread_id_buffer.iter_mut().for_each(|v| *v = i32::MIN); + self.thread_id_buffer.iter_mut().for_each(|v| *v = -1); self.store_counts.iter_mut().for_each(|c| *c = 0); self.load_counts.iter_mut().for_each(|c| *c = 0); self.applied_store_k = 0; @@ -1332,22 +1339,28 @@ impl ThreadState { self.applied_op_mode = Some(op_mode); } - pub fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { + pub fn to_rgba(&self, thread_id_filter: String) -> Vec { let FuncGeometry { width, height, .. } = self.geom; - let domain = &self.thread_ids; - let range = colorous::TABLEAU10; - let mut out = vec![0u8; width * height * 4]; + let filter_id: Option = thread_id_filter.parse::().ok(); for (chunk, &thread_id) in out.chunks_exact_mut(4).zip(self.thread_id_buffer.iter()) { - let idx = domain.binary_search(&thread_id); - - if let Ok(i) = idx { - let color = range[i]; + let color = self + .global_thread_ids + .binary_search(&thread_id) + .ok() + .filter(|&rank| rank < colorous::SET3.len()) + .map(|rank| colorous::SET3[rank]); + + if let Some(color) = color { chunk[0] = color.r; chunk[1] = color.g; chunk[2] = color.b; - chunk[3] = 255; + chunk[3] = if filter_id == Some(thread_id) || filter_id == Some(-1) { + 255 + } else { + 64 + }; } else { chunk[0] = 0; chunk[1] = 0; diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 2297ea2eaed6..33ae8fc0806e 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -446,9 +446,6 @@ impl Trace { // Loads we deferred for DAG inference. let mut pending_loads: Vec<(String, i32)> = Vec::new(); - // Parallel task starts we deferred for accumulating thread IDs by Func. - let mut pending_parallel_tasks: Vec<(i32, i32)> = Vec::new(); - // Packet parsing loop. while pos + HEADER_BYTES <= total { let size = u32_le(data, pos) as usize; @@ -479,7 +476,9 @@ impl Trace { bits: data[pos + 17], lanes: u16_le(data, pos + 18), }; - (type_, 0) + + // Initialize thread_ids as a sentinel value of -1. + (type_, -1) } else { ( HalideType { @@ -657,7 +656,6 @@ impl Trace { } } EventCode::BeginParallelTask => { - pending_parallel_tasks.push((thread_id, parent_id)); thread_id_by_task_id.insert(id, thread_id); } _ => {} @@ -691,25 +689,9 @@ impl Trace { } } - // Compute the set of thread IDs used to compute each Func. - for (thread_id, parent_id) in &pending_parallel_tasks { - let mut current = *parent_id; - loop { - match id_to_info.get(¤t) { - Some((EventCode::Produce, producing_func, _)) => { - thread_ids_by_func - .entry(producing_func.clone()) - .or_default() - .insert(*thread_id); - break; - } - Some((_, _, next_parent)) => current = *next_parent, - None => break, - } - } - } - - // Resolve the executing thread for each load/store packet by walking its parent chain. + // Resolve the executing thread for each load/store packet by walking its parent chain up + // to the nearest enclosing `BeginParallelTask`. Exclude any stores / loads that do not + // have a meaningful thread_id (denoted by a sentinel value of -1). for pkt in packets.iter_mut() { if !pkt.is_load_or_store() { continue; @@ -727,6 +709,13 @@ impl Trace { None => break, } } + + if pkt.thread_id != -1 { + thread_ids_by_func + .entry(pkt.func.clone()) + .or_default() + .insert(pkt.thread_id); + } } // Compute max per-pixel store/load counts for each Func using the index lists. We extract diff --git a/apps/halidoscope/src/App.tsx b/apps/halidoscope/src/App.tsx index ddb80c089a02..af33ddb9b4ce 100644 --- a/apps/halidoscope/src/App.tsx +++ b/apps/halidoscope/src/App.tsx @@ -20,6 +20,7 @@ function App() { global_max_load_count: 0, global_max_redundant_store_count: 0, global_max_reuse_distance: 0, + global_thread_ids: [], }); const setActiveFunc = useSetAtom(funcAtom); diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 01e3be3cdd49..0ec9919eddbf 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -152,6 +152,7 @@ function FuncNode({ data }: NodeProps>) { result = await renderThread({ ...params, threadOpMode: thread.op, + threadId: thread.id, }); break; @@ -195,7 +196,7 @@ function FuncNode({ data }: NodeProps>) { render, activeFunc, setTabularData, - thread.op, + thread, ]); React.useEffect(() => { diff --git a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx index 0b5534a9216b..6d188c643001 100644 --- a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -1,3 +1,4 @@ +import * as d3 from "d3"; import { useAtomValue } from "jotai"; import { Separator } from "radix-ui"; import * as React from "react"; @@ -15,7 +16,7 @@ import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; import { type RenderMode as RM, renderAtom } from "@/state/render"; import { tabularDataAtom } from "@/state/tabularData"; -import { threadAtom } from "@/state/thread"; +import { threadAtom, NO_THREAD_INFO_SENTINEL_ID } from "@/state/thread"; const RENDER_MODE_TO_LABEL: Record = { Grayscale: "", @@ -31,18 +32,21 @@ interface HistogramData { type: "Histogram"; data: { x1: number; x2: number; y: number }[]; domain: [number, number]; + lut: Record; } interface BarChartData { type: "Bar Chart"; data: { x: string; y: number }[]; domain: string[]; + lut: Record; } interface NoChartData { type: "No Chart"; data: number[]; domain: [number, number]; + lut: Record; } type ChartData = HistogramData | BarChartData | NoChartData; @@ -67,12 +71,13 @@ function VisualizationPanel() { y: histogramData?.[i] ?? 0, })), domain: [min, max + 1], + lut: {}, }; }, [min], ); - const { type, data, domain } = React.useMemo((): ChartData => { + const { type, data, domain, lut } = React.useMemo((): ChartData => { switch (render.renderMode) { case "Store Frequency": { const max = @@ -119,10 +124,18 @@ function VisualizationPanel() { y: thread.op === "Store" ? storeCounts[i] : loadCounts[i], })), domain: threadIds.map((tId) => `${tId}`), + lut: stats.global_thread_ids.reduce>( + (acc, el, i) => { + acc[el] = d3.schemeSet3[i]; + + return acc; + }, + {}, + ), }; } default: { - return { type: "No Chart", data: [], domain: [-1, -1] }; + return { type: "No Chart", data: [], domain: [-1, -1], lut: {} }; } } }, [ @@ -140,35 +153,47 @@ function VisualizationPanel() { case "Histogram": return ( <> - - + + + + + + ); case "Bar Chart": return ( <> - - + + + + + x === thread.id || thread.id === NO_THREAD_INFO_SENTINEL_ID + } + /> + + ); case "No Chart": - return null; + return ; } - }, [type, data, domain, render.renderMode, thread.op]); + }, [type, data, domain, lut, render.renderMode, thread]); return (
@@ -176,7 +201,6 @@ function VisualizationPanel() { {renderChart()} - diff --git a/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx b/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx index 38bd0b003dc6..bced6ec49700 100644 --- a/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx +++ b/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx @@ -9,9 +9,11 @@ interface BarChartProps { x: string; y: string; }; + lut: Record; + highlight?: (x: string) => boolean; } -function BarChart({ data, domain, labels }: BarChartProps) { +function BarChart({ data, domain, labels, lut, highlight }: BarChartProps) { const ref = React.useRef(null); React.useEffect(() => { @@ -40,11 +42,14 @@ function BarChart({ data, domain, labels }: BarChartProps) { tickRotate: -45, type: "band", }, - color: { - scheme: "Tableau10", - type: "ordinal", - }, - marks: [Plot.barY(data, { x: "x", y: "y", fill: "x" })], + marks: [ + Plot.barY(data, { + x: "x", + y: "y", + fill: (d) => lut[d.x] ?? "#000000", + fillOpacity: (d) => (highlight?.(d.x) ? 1 : 0.25), + }), + ], }); ref.current.append(plot); @@ -52,7 +57,7 @@ function BarChart({ data, domain, labels }: BarChartProps) { return () => { plot.remove(); }; - }, [data, labels, domain]); + }, [data, labels, domain, lut, highlight]); return data.every((d) => d.y === 0) ? (
diff --git a/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx b/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx index 87917fc93a55..e3839d219906 100644 --- a/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx +++ b/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx @@ -4,7 +4,7 @@ import { useAtom } from "jotai"; import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; -import { threadAtom } from "@/state/thread"; +import { threadAtom, NO_THREAD_INFO_SENTINEL_ID } from "@/state/thread"; function BarChartParameters() { const { funcs } = useTraceContext(); @@ -12,7 +12,7 @@ function BarChartParameters() { const [thread, setThread] = useAtom(threadAtom); return ( -
+
Selected Func @@ -55,50 +55,105 @@ function BarChartParameters() {
-
- - Operation - - { - setThread({ ...thread, op: value as "Load" | "Store" }); - }} - > - +
+ + Operation + + { + setThread({ ...thread, op: value as "Load" | "Store" }); + }} > - - - - - - - - + + + + + + + + + + + Store + + + Load + + + + +
+
+ + Filter by Thread + + { + setThread({ + ...thread, + id: value === "None" ? NO_THREAD_INFO_SENTINEL_ID : value, + }); + }} > - - - Store - - - Load - - - - + + + + + + + + + + + + None + + {funcs[activeFunc].thread_ids + .filter((threadId) => threadId !== "0") + .map((threadId) => ( + + {threadId} + + ))} + + + +
); diff --git a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx index 7e64f1aa8a8e..1e74bf2f7483 100644 --- a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx +++ b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx @@ -39,6 +39,7 @@ function Histogram({ data, domain, labels }: HistogramProps) { style: { fontSize: "12px", }, + width: 480, marginBottom: 60, y: { grid: true, diff --git a/apps/halidoscope/src/hooks/trace.ts b/apps/halidoscope/src/hooks/trace.ts index 477118891c34..f033ca76784c 100644 --- a/apps/halidoscope/src/hooks/trace.ts +++ b/apps/halidoscope/src/hooks/trace.ts @@ -16,6 +16,7 @@ const TraceContext = React.createContext<{ global_max_load_count: 0, global_max_redundant_store_count: 0, global_max_reuse_distance: 0, + global_thread_ids: [], }, }); diff --git a/apps/halidoscope/src/state/thread.ts b/apps/halidoscope/src/state/thread.ts index 0a7747964b97..abb06670739e 100644 --- a/apps/halidoscope/src/state/thread.ts +++ b/apps/halidoscope/src/state/thread.ts @@ -1,7 +1,8 @@ import { atom } from "jotai"; export type ThreadOpMode = "Store" | "Load"; -export const threadAtom = atom<{ id: number; op: ThreadOpMode }>({ - id: 0, +export const NO_THREAD_INFO_SENTINEL_ID = "-1"; +export const threadAtom = atom<{ id: string; op: ThreadOpMode }>({ + id: NO_THREAD_INFO_SENTINEL_ID, op: "Store", }); diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts index 35f6523dd558..d6b3f4c90536 100644 --- a/apps/halidoscope/src/types/index.ts +++ b/apps/halidoscope/src/types/index.ts @@ -21,7 +21,7 @@ export interface FuncMeta extends Record { produce_ranges: IndexRange[]; consume_ranges: IndexRange[]; thread_count: number; - thread_ids: number[]; + thread_ids: string[]; } export interface StatsMeta { @@ -29,6 +29,7 @@ export interface StatsMeta { global_max_load_count: number; global_max_redundant_store_count: number; global_max_reuse_distance: number; + global_thread_ids: string[]; } export interface TraceMeta { diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index 854f283b129b..8d8b4b4a512d 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -217,6 +217,7 @@ export async function renderInf({ export interface RenderThreadFuncParams extends RenderFuncParams { threadOpMode: ThreadOpMode; + threadId: string; } export async function renderThread({ @@ -224,6 +225,7 @@ export async function renderThread({ globalIndex, normalizationMode, threadOpMode, + threadId, width, height, includeTabularData, @@ -233,6 +235,7 @@ export async function renderThread({ globalIndex, normalizationMode, opMode: threadOpMode, + threadId: threadId, }); return splitTensorDataAndTabularData({ From aceaa66437b2968daadb5d6e468aee9dda3364bb Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 23 Jul 2026 15:30:02 -0700 Subject: [PATCH 30/67] Back out changes to Python tracing infrastructure that are not on main and not used by Halidoscope. --- python_bindings/src/halide/CMakeLists.txt | 1 - .../src/halide/halide_/PyHalide.cpp | 2 - .../src/halide/halide_/PyTrace.cpp | 658 ------------------ python_bindings/src/halide/halide_/PyTrace.h | 14 - 4 files changed, 675 deletions(-) delete mode 100644 python_bindings/src/halide/halide_/PyTrace.cpp delete mode 100644 python_bindings/src/halide/halide_/PyTrace.h diff --git a/python_bindings/src/halide/CMakeLists.txt b/python_bindings/src/halide/CMakeLists.txt index f572722cde66..138625bbf149 100644 --- a/python_bindings/src/halide/CMakeLists.txt +++ b/python_bindings/src/halide/CMakeLists.txt @@ -33,7 +33,6 @@ target_sources( halide_/PySerialization.cpp halide_/PyStage.cpp halide_/PyTarget.cpp - halide_/PyTrace.cpp halide_/PyTuple.cpp halide_/PyType.cpp halide_/PyVar.cpp diff --git a/python_bindings/src/halide/halide_/PyHalide.cpp b/python_bindings/src/halide/halide_/PyHalide.cpp index 3a6c0dbea802..dc2755bf6994 100644 --- a/python_bindings/src/halide/halide_/PyHalide.cpp +++ b/python_bindings/src/halide/halide_/PyHalide.cpp @@ -23,7 +23,6 @@ #include "PyRDom.h" #include "PySerialization.h" #include "PyTarget.h" -#include "PyTrace.h" #include "PyTuple.h" #include "PyType.h" #include "PyVar.h" @@ -78,7 +77,6 @@ PYBIND11_MODULE(HALIDE_PYBIND_MODULE_NAME, m) { define_derivative(m); define_generator(m); define_serialization(m); - define_trace(m); // There is no PyUtil yet, so just put this here m.def("load_plugin", &Halide::load_plugin, py::arg("lib_name")); diff --git a/python_bindings/src/halide/halide_/PyTrace.cpp b/python_bindings/src/halide/halide_/PyTrace.cpp deleted file mode 100644 index 6eb8db9b06de..000000000000 --- a/python_bindings/src/halide/halide_/PyTrace.cpp +++ /dev/null @@ -1,658 +0,0 @@ -#include "PyTrace.h" - -#include "HalideRuntime.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Halide::PythonBindings { - -// Statistics about a traced Func -struct FuncStats { - std::string name; - std::vector min_coords; - std::vector max_coords; - std::optional min_value; - std::optional max_value; -}; - -// A single trace packet -struct TracePacket { - int32_t id; - int32_t event; - int32_t parent_id; - int32_t value_index; - uint8_t type_code; - uint8_t type_bits; - uint16_t type_lanes; - std::vector coordinates; - std::vector value; - std::string func; - std::string trace_tag; - - bool is_load() const { - return event == halide_trace_load; - } - bool is_store() const { - return event == halide_trace_store; - } - bool is_load_or_store() const { - return is_load() || is_store(); - } - - py::object get_values() const { - if (value.empty()) { - return py::list(); - } - - py::list result; - const size_t elem_size = (type_bits + 7) / 8; - const size_t count = type_lanes; - - for (size_t i = 0; i < count && (i + 1) * elem_size <= value.size(); ++i) { - const uint8_t *ptr = value.data() + i * elem_size; - if (type_code == halide_type_float) { - if (type_bits == 32) { - float v; - std::memcpy(&v, ptr, sizeof(v)); - result.append(v); - } else if (type_bits == 64) { - double v; - std::memcpy(&v, ptr, sizeof(v)); - result.append(v); - } - } else if (type_code == halide_type_int) { - if (type_bits == 8) { - result.append(static_cast(*ptr)); - } else if (type_bits == 16) { - int16_t v; - std::memcpy(&v, ptr, sizeof(v)); - result.append(v); - } else if (type_bits == 32) { - int32_t v; - std::memcpy(&v, ptr, sizeof(v)); - result.append(v); - } else if (type_bits == 64) { - int64_t v; - std::memcpy(&v, ptr, sizeof(v)); - result.append(v); - } - } else if (type_code == halide_type_uint) { - if (type_bits == 8) { - result.append(*ptr); - } else if (type_bits == 16) { - uint16_t v; - std::memcpy(&v, ptr, sizeof(v)); - result.append(v); - } else if (type_bits == 32) { - uint32_t v; - std::memcpy(&v, ptr, sizeof(v)); - result.append(v); - } else if (type_bits == 64) { - uint64_t v; - std::memcpy(&v, ptr, sizeof(v)); - result.append(v); - } - } - } - return result; - } -}; - -// A complete Halide trace -class Trace { -public: - static Trace load(const std::string &path, - std::optional> progress_callback = std::nullopt) { - std::ifstream file(path, std::ios::binary | std::ios::ate); - if (!file) { - throw std::runtime_error("Failed to open trace file: " + path); - } - - const size_t total_size = file.tellg(); - file.seekg(0); - - std::vector data(total_size); - file.read(reinterpret_cast(data.data()), total_size); - - return load_from_memory(data.data(), total_size, progress_callback); - } - - static Trace load_from_memory(const uint8_t *data, size_t total_size, - std::optional> progress_callback = std::nullopt) { - Trace trace; - - // String interning table - std::map string_to_index; - auto intern_string = [&](const std::string &s) -> size_t { - auto it = string_to_index.find(s); - if (it != string_to_index.end()) { - return it->second; - } - size_t idx = trace.strings_.size(); - trace.strings_.push_back(s); - string_to_index[s] = idx; - return idx; - }; - - // Pipeline tracking for qualified names - std::map parent_to_pipeline; - // For DAG inference: packet_id -> (event, qualified_name, parent_id) - std::map> id_to_info; - // LOADs to process for DAG - std::vector> load_packets; - - size_t pos = 0; - size_t last_progress = 0; - const size_t progress_interval = std::max(size_t(1), total_size / 100); - - while (pos + sizeof(halide_trace_packet_t) <= total_size) { - const auto *pkt_ptr = reinterpret_cast(data + pos); - - if (pkt_ptr->size < sizeof(halide_trace_packet_t) || pos + pkt_ptr->size > total_size) { - break; - } - - // Use the halide_trace_packet_t helper methods - std::string func_name(pkt_ptr->func()); - std::string trace_tag(pkt_ptr->trace_tag()); - const auto ev = static_cast(pkt_ptr->event); - - // Track pipeline hierarchy - if (ev == halide_trace_begin_pipeline) { - trace.pipelines_[pkt_ptr->id] = func_name; - parent_to_pipeline[pkt_ptr->id] = func_name; - } else if (ev == halide_trace_end_pipeline) { - parent_to_pipeline.erase(pkt_ptr->parent_id); - } else if (parent_to_pipeline.count(pkt_ptr->parent_id)) { - parent_to_pipeline[pkt_ptr->id] = parent_to_pipeline[pkt_ptr->parent_id]; - } - - // Build qualified name - std::string qualified; - auto pipeline_it = parent_to_pipeline.find(pkt_ptr->parent_id); - if (pipeline_it != parent_to_pipeline.end() && !pipeline_it->second.empty()) { - qualified = pipeline_it->second + ":" + func_name; - } else { - qualified = func_name; - } - - // Record for DAG inference - id_to_info[pkt_ptr->id] = {pkt_ptr->event, qualified, pkt_ptr->parent_id}; - - // Handle event types - if (ev == halide_trace_tag && trace_tag.rfind("func_type_and_dim:", 0) == 0) { - parse_func_type_and_dim(qualified, trace_tag, trace.funcs_); - } else if (ev == halide_trace_begin_realization) { - if (trace.funcs_.find(qualified) == trace.funcs_.end()) { - trace.funcs_[qualified] = FuncStats{qualified}; - } - if (pipeline_it != parent_to_pipeline.end()) { - parent_to_pipeline[pkt_ptr->id] = pipeline_it->second; - } - } else if (ev == halide_trace_produce || ev == halide_trace_consume || - ev == halide_trace_end_produce || ev == halide_trace_end_consume) { - if (pipeline_it != parent_to_pipeline.end()) { - parent_to_pipeline[pkt_ptr->id] = pipeline_it->second; - } - } else if (ev == halide_trace_load) { - load_packets.emplace_back(func_name, pkt_ptr->parent_id); - if (trace.funcs_.find(qualified) == trace.funcs_.end()) { - trace.funcs_[qualified] = FuncStats{qualified}; - } - update_stats_inline(pkt_ptr, trace.funcs_[qualified]); - } else if (ev == halide_trace_store) { - if (trace.funcs_.find(qualified) == trace.funcs_.end()) { - trace.funcs_[qualified] = FuncStats{qualified}; - } - update_stats_inline(pkt_ptr, trace.funcs_[qualified]); - } - - // Build packet - TracePacket pkt; - pkt.id = pkt_ptr->id; - pkt.event = pkt_ptr->event; - pkt.parent_id = pkt_ptr->parent_id; - pkt.value_index = pkt_ptr->value_index; - pkt.type_code = pkt_ptr->type.code; - pkt.type_bits = pkt_ptr->type.bits; - pkt.type_lanes = pkt_ptr->type.lanes; - - // Copy coordinates using the helper method - if (pkt_ptr->dimensions > 0) { - pkt.coordinates.resize(pkt_ptr->dimensions); - std::memcpy(pkt.coordinates.data(), pkt_ptr->coordinates(), - pkt_ptr->dimensions * sizeof(int32_t)); - } - - // Copy value bytes using the helper method - const size_t value_bytes = pkt_ptr->type.lanes * pkt_ptr->type.bytes(); - if (value_bytes > 0) { - pkt.value.resize(value_bytes); - std::memcpy(pkt.value.data(), pkt_ptr->value(), value_bytes); - } - - // Intern strings - pkt.func = trace.strings_[intern_string(func_name)]; - if (!trace_tag.empty()) { - pkt.trace_tag = trace.strings_[intern_string(trace_tag)]; - } - - trace.packets_.push_back(std::move(pkt)); - - pos += pkt_ptr->size; - - // Progress callback - if (progress_callback && pos - last_progress >= progress_interval) { - (*progress_callback)(pos, total_size); - last_progress = pos; - } - } - - // DAG inference - for (const auto &[func_name, load_parent_id] : load_packets) { - auto pipeline_it = parent_to_pipeline.find(load_parent_id); - std::string loaded_func; - if (pipeline_it != parent_to_pipeline.end() && !pipeline_it->second.empty()) { - loaded_func = pipeline_it->second + ":" + func_name; - } else { - loaded_func = func_name; - } - - int32_t current_id = load_parent_id; - while (id_to_info.count(current_id)) { - const auto &[ev, producing_func, next_parent] = id_to_info[current_id]; - if (ev == halide_trace_produce) { - if (loaded_func != producing_func) { - trace.dag_edges_[loaded_func].insert(producing_func); - } - break; - } - current_id = next_parent; - } - } - - if (progress_callback) { - (*progress_callback)(total_size, total_size); - } - - return trace; - } - - size_t size() const { - return packets_.size(); - } - - const TracePacket &operator[](size_t i) const { - if (i >= packets_.size()) { - throw std::out_of_range("Packet index out of range"); - } - return packets_[i]; - } - - const std::map &funcs() const { - return funcs_; - } - const std::map &pipelines() const { - return pipelines_; - } - const std::map> &dag_edges() const { - return dag_edges_; - } - const std::vector &packets() const { - return packets_; - } - - std::vector filter_loads_stores() const { - std::vector result; - for (const auto &p : packets_) { - if (p.is_load_or_store()) { - result.push_back(p); - } - } - return result; - } - - // Returns the indices of all store packets, in order. - // Cached once at load time in Python to avoid iterating all packets per render. - std::vector store_indices() const { - std::vector result; - result.reserve(packets_.size() / 4); - for (size_t i = 0; i < packets_.size(); ++i) { - if (packets_[i].is_store()) { - result.push_back(i); - } - } - return result; - } - - // Returns the indices of all load packets, in order. - // Cached once at load time in Python to avoid iterating all packets per render. - std::vector load_indices() const { - std::vector result; - result.reserve(packets_.size() / 4); - for (size_t i = 0; i < packets_.size(); ++i) { - if (packets_[i].is_load()) { - result.push_back(i); - } - } - return result; - } - - // Returns the maximum store count and maximum load count per Func across all - // pixels. Runs entirely in C++ to avoid per-packet Python/pybind11 overhead. - // Result: dict keyed by qualified func name, each value a dict with: - // max_store_count: int - // max_load_count: int - py::dict compute_max_load_store_counts() const { - // Map unqualified name -> FuncStats* for packet lookup. - // (TracePacket.func is always the unqualified name; funcs_ keys are qualified.) - std::map unqualified_to_stats; - for (const auto &[name, stats] : funcs_) { - auto colon = name.rfind(':'); - std::string unqualified = (colon != std::string::npos) ? name.substr(colon + 1) : name; - unqualified_to_stats.emplace(unqualified, &stats); - } - - struct FuncAccum { - std::vector store_counts; - std::vector load_counts; - int32_t width; - int32_t height; - int32_t min_x; - int32_t min_y; - }; - - std::map accum; - for (const auto &[qualified, stats] : funcs_) { - if (stats.min_coords.empty() || stats.max_coords.empty()) continue; - const int32_t width = stats.max_coords[0] - stats.min_coords[0]; - const int32_t height = - (stats.min_coords.size() > 1 && stats.max_coords.size() > 1) ? stats.max_coords[1] - stats.min_coords[1] : 1; - if (width <= 0 || height <= 0) continue; - accum[qualified] = FuncAccum{ - std::vector(height * width, 0), - std::vector(height * width, 0), - width, - height, - stats.min_coords[0], - (stats.min_coords.size() > 1) ? stats.min_coords[1] : 0, - }; - } - - for (const auto &pkt : packets_) { - if (!pkt.is_load_or_store()) continue; - - auto stats_it = unqualified_to_stats.find(pkt.func); - if (stats_it == unqualified_to_stats.end()) continue; - - auto accum_it = accum.find(stats_it->second->name); - if (accum_it == accum.end()) continue; - - FuncAccum &fa = accum_it->second; - const int32_t n_lanes = std::max(1, (int32_t)pkt.type_lanes); - const int32_t dims_per_lane = (int32_t)pkt.coordinates.size() / n_lanes; - int32_t *arr = pkt.is_store() ? fa.store_counts.data() : fa.load_counts.data(); - - for (int32_t l = 0; l < n_lanes; ++l) { - const int32_t x = pkt.coordinates[l] - fa.min_x; - const int32_t y = (dims_per_lane >= 2) ? pkt.coordinates[n_lanes + l] - fa.min_y : -fa.min_y; - if (x >= 0 && x < fa.width && y >= 0 && y < fa.height) { - arr[y * fa.width + x]++; - } - } - } - - py::dict result; - for (const auto &[qualified, fa] : accum) { - const int32_t max_store = *std::max_element(fa.store_counts.begin(), fa.store_counts.end()); - const int32_t max_load = *std::max_element(fa.load_counts.begin(), fa.load_counts.end()); - py::dict entry; - entry["max_store_count"] = max_store; - entry["max_load_count"] = max_load; - result[py::cast(qualified)] = entry; - } - return result; - } - - std::string dag_as_dot() const { - std::ostringstream ss; - ss << "digraph dag {\n"; - ss << " rankdir=\"LR\";\n"; - ss << " node [shape=box];\n"; - - auto sanitize = [](const std::string &name) { - std::string result = name; - for (char &c : result) { - if (c == ':') c = '_'; - } - return result; - }; - - auto label = [](const std::string &name) { - auto pos = name.rfind(':'); - return (pos != std::string::npos) ? name.substr(pos + 1) : name; - }; - - for (const auto &[func, _] : funcs_) { - ss << " " << sanitize(func) << " [label=\"" << label(func) << "\"];\n"; - } - - for (const auto &[src, dsts] : dag_edges_) { - for (const auto &dst : dsts) { - ss << " " << sanitize(src) << " -> " << sanitize(dst) << ";\n"; - } - } - - ss << "}\n"; - return ss.str(); - } - -private: - std::vector packets_; - std::map funcs_; - std::map pipelines_; - std::map> dag_edges_; - std::vector strings_; // Interned strings - - static void parse_func_type_and_dim(const std::string &qualified, - const std::string &trace_tag, - std::map &funcs) { - std::istringstream iss(trace_tag); - std::string prefix; - iss >> prefix; // "func_type_and_dim:" - - int num_types; - if (!(iss >> num_types)) return; - - // Skip type info - for (int i = 0; i < num_types * 3; ++i) { - int dummy; - if (!(iss >> dummy)) return; - } - - int num_dims; - if (!(iss >> num_dims)) return; - - std::vector min_coords, max_coords; - for (int i = 0; i < num_dims; ++i) { - int min_val, extent; - if (!(iss >> min_val >> extent)) break; - min_coords.push_back(min_val); - max_coords.push_back(min_val + extent); - } - - if (!min_coords.empty()) { - if (funcs.find(qualified) == funcs.end()) { - funcs[qualified] = FuncStats{qualified}; - } - funcs[qualified].min_coords = std::move(min_coords); - funcs[qualified].max_coords = std::move(max_coords); - } - } - - static void update_stats_inline(const halide_trace_packet_t *pkt, - FuncStats &stats) { - // Update coordinate ranges using the helper method. - // Coordinates are dim-major: [x0..xL, y0..yL, c0..cL] where L = type.lanes. - // pkt->dimensions = logical_dims * lanes, so we must stride by lanes to get - // the correct coordinate for each logical dimension. - if (pkt->dimensions > 0) { - const int *coords = pkt->coordinates(); - const int n_lanes = std::max(1, static_cast(pkt->type.lanes)); - const int logical_dims = pkt->dimensions / n_lanes; - if (stats.min_coords.empty()) { - stats.min_coords.resize(logical_dims); - stats.max_coords.resize(logical_dims); - for (int d = 0; d < logical_dims; ++d) { - int mn = coords[d * n_lanes]; - int mx = coords[d * n_lanes] + 1; - for (int l = 1; l < n_lanes; ++l) { - mn = std::min(mn, coords[d * n_lanes + l]); - mx = std::max(mx, coords[d * n_lanes + l] + 1); - } - stats.min_coords[d] = mn; - stats.max_coords[d] = mx; - } - } else { - for (int d = 0; d < logical_dims && d < static_cast(stats.min_coords.size()); ++d) { - for (int l = 0; l < n_lanes; ++l) { - const int coord = coords[d * n_lanes + l]; - stats.min_coords[d] = std::min(stats.min_coords[d], coord); - stats.max_coords[d] = std::max(stats.max_coords[d], coord + 1); - } - } - } - } - - // Update value ranges using the helper method - const uint8_t *val_ptr = static_cast(pkt->value()); - const size_t elem_size = pkt->type.bytes(); - - for (uint16_t i = 0; i < pkt->type.lanes; ++i) { - double val = 0; - const uint8_t *ptr = val_ptr + i * elem_size; - - if (pkt->type.code == halide_type_float) { - if (pkt->type.bits == 32) { - float v; - std::memcpy(&v, ptr, sizeof(v)); - val = v; - } else if (pkt->type.bits == 64) { - std::memcpy(&val, ptr, sizeof(val)); - } else { - continue; - } - } else if (pkt->type.code == halide_type_int) { - if (pkt->type.bits == 8) { - val = static_cast(*ptr); - } else if (pkt->type.bits == 16) { - int16_t v; - std::memcpy(&v, ptr, sizeof(v)); - val = v; - } else if (pkt->type.bits == 32) { - int32_t v; - std::memcpy(&v, ptr, sizeof(v)); - val = v; - } else if (pkt->type.bits == 64) { - int64_t v; - std::memcpy(&v, ptr, sizeof(v)); - val = static_cast(v); - } else { - continue; - } - } else if (pkt->type.code == halide_type_uint) { - if (pkt->type.bits == 8) { - val = *ptr; - } else if (pkt->type.bits == 16) { - uint16_t v; - std::memcpy(&v, ptr, sizeof(v)); - val = v; - } else if (pkt->type.bits == 32) { - uint32_t v; - std::memcpy(&v, ptr, sizeof(v)); - val = v; - } else if (pkt->type.bits == 64) { - uint64_t v; - std::memcpy(&v, ptr, sizeof(v)); - val = static_cast(v); - } else { - continue; - } - } else { - continue; - } - - if (!stats.min_value.has_value()) { - stats.min_value = val; - stats.max_value = val; - } else { - stats.min_value = std::min(*stats.min_value, val); - stats.max_value = std::max(*stats.max_value, val); - } - } - } -}; - -void define_trace(py::module &m) { - py::class_(m, "FuncStats") - .def_readonly("name", &FuncStats::name) - .def_readonly("min_coords", &FuncStats::min_coords) - .def_readonly("max_coords", &FuncStats::max_coords) - .def_property_readonly("min_value", [](const FuncStats &s) -> py::object { - return s.min_value.has_value() ? py::cast(*s.min_value) : py::none(); - }) - .def_property_readonly("max_value", [](const FuncStats &s) -> py::object { - return s.max_value.has_value() ? py::cast(*s.max_value) : py::none(); - }); - - py::class_(m, "TracePacket") - .def_readonly("id", &TracePacket::id) - .def_readonly("event", &TracePacket::event) - .def_readonly("parent_id", &TracePacket::parent_id) - .def_readonly("value_index", &TracePacket::value_index) - .def_readonly("type_code", &TracePacket::type_code) - .def_readonly("type_bits", &TracePacket::type_bits) - .def_readonly("type_lanes", &TracePacket::type_lanes) - .def_readonly("coordinates", &TracePacket::coordinates) - .def_readonly("func", &TracePacket::func) - .def_readonly("trace_tag", &TracePacket::trace_tag) - .def_property_readonly("is_load", &TracePacket::is_load) - .def_property_readonly("is_store", &TracePacket::is_store) - .def_property_readonly("is_load_or_store", &TracePacket::is_load_or_store) - .def("get_values", &TracePacket::get_values); - - py::class_(m, "Trace") - .def_static("load", [](const std::string &path, py::object progress_callback) { - if (progress_callback.is_none()) { - return Trace::load(path); - } - return Trace::load(path, [&](size_t bytes_read, size_t total_bytes) { - progress_callback(bytes_read, total_bytes); - }); }, py::arg("path"), py::arg("progress_callback") = py::none()) - .def_static("load_bytes", [](py::bytes data) { - std::string str = data; - return Trace::load_from_memory( - reinterpret_cast(str.data()), - str.size(), - std::nullopt); }, py::arg("data")) - .def("__len__", &Trace::size) - .def("__getitem__", &Trace::operator[], py::arg("index")) - .def_property_readonly("funcs", &Trace::funcs) - .def_property_readonly("pipelines", &Trace::pipelines) - .def_property_readonly("dag_edges", &Trace::dag_edges) - .def_property_readonly("packets", &Trace::packets) - .def("filter_loads_stores", &Trace::filter_loads_stores) - .def("store_indices", &Trace::store_indices) - .def("load_indices", &Trace::load_indices) - .def("dag_as_dot", &Trace::dag_as_dot) - .def("compute_max_load_store_counts", &Trace::compute_max_load_store_counts); -} - -} // namespace Halide::PythonBindings diff --git a/python_bindings/src/halide/halide_/PyTrace.h b/python_bindings/src/halide/halide_/PyTrace.h deleted file mode 100644 index da7c5f0fdd57..000000000000 --- a/python_bindings/src/halide/halide_/PyTrace.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef HALIDE_PYTHON_BINDINGS_PYTRACE_H -#define HALIDE_PYTHON_BINDINGS_PYTRACE_H - -#include "PyHalide.h" - -namespace Halide { -namespace PythonBindings { - -void define_trace(py::module &m); - -} // namespace PythonBindings -} // namespace Halide - -#endif // HALIDE_PYTHON_BINDINGS_PYTRACE_H From b743763d8fea568c5bf1fbff7f6cd93aab6e1f37 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Sun, 26 Jul 2026 12:00:44 -0700 Subject: [PATCH 31/67] Add support for visualizing information from the Halide profiler. --- apps/halidoscope/package.json | 2 + apps/halidoscope/pnpm-lock.yaml | 23 +++ apps/halidoscope/src-tauri/src/commands.rs | 55 +++++- apps/halidoscope/src-tauri/src/lib.rs | 3 +- apps/halidoscope/src-tauri/tauri.conf.json | 9 +- apps/halidoscope/src/App.css | 8 +- apps/halidoscope/src/App.tsx | 95 ++++++++--- .../src/components/canvas/Canvas.tsx | 12 +- .../src/components/canvas/FuncEdge.tsx | 4 +- .../src/components/canvas/FuncNode.tsx | 12 +- .../src/components/controls/ControlTabs.tsx | 4 +- .../components/views/profiler/Profiler.tsx | 15 ++ .../src/components/views/profiler/Treemap.tsx | 161 ++++++++++++++++++ .../controls/ProfilerControlPanel.tsx | 63 +++++++ .../src/components/views/tracer/Tracer.tsx | 2 +- apps/halidoscope/src/hooks/profile.ts | 10 ++ apps/halidoscope/src/state/profile-metric.ts | 16 ++ apps/halidoscope/src/types/index.ts | 35 ++++ apps/halidoscope/src/utils/api.ts | 6 +- 19 files changed, 492 insertions(+), 43 deletions(-) create mode 100644 apps/halidoscope/src/components/views/profiler/Profiler.tsx create mode 100644 apps/halidoscope/src/components/views/profiler/Treemap.tsx create mode 100644 apps/halidoscope/src/components/views/profiler/controls/ProfilerControlPanel.tsx create mode 100644 apps/halidoscope/src/hooks/profile.ts create mode 100644 apps/halidoscope/src/state/profile-metric.ts diff --git a/apps/halidoscope/package.json b/apps/halidoscope/package.json index b8d33747f348..66aeb897bd30 100644 --- a/apps/halidoscope/package.json +++ b/apps/halidoscope/package.json @@ -23,6 +23,7 @@ "clsx": "^2.1.1", "d3": "^7.9.0", "jotai": "^2.20.1", + "lodash-es": "^4.18.1", "radix-ui": "^1.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -32,6 +33,7 @@ "@eslint/js": "^10.0.1", "@tauri-apps/cli": "^2.11.4", "@types/d3": "^7.4.3", + "@types/lodash-es": "^4.17.12", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", diff --git a/apps/halidoscope/pnpm-lock.yaml b/apps/halidoscope/pnpm-lock.yaml index b295218cec07..0d1fe3953fac 100644 --- a/apps/halidoscope/pnpm-lock.yaml +++ b/apps/halidoscope/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: jotai: specifier: ^2.20.1 version: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.15)(react@19.2.6) + lodash-es: + specifier: ^4.18.1 + version: 4.18.1 radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -60,6 +63,9 @@ importers: '@types/d3': specifier: ^7.4.3 version: 7.4.3 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 '@types/react': specifier: ^19.1.8 version: 19.2.15 @@ -1384,6 +1390,12 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1972,6 +1984,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3656,6 +3671,12 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.24 + + '@types/lodash@4.17.24': {} + '@types/react-dom@19.2.3(@types/react@19.2.15)': dependencies: '@types/react': 19.2.15 @@ -4253,6 +4274,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.18.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 9e9259661d34..b68e3c321b41 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -5,7 +5,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::sync::Mutex; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tauri::ipc::Response; use tauri::State; @@ -569,3 +569,56 @@ pub fn render_thread( load_counts, ))) } + +// ── Profiler ───────────────────────────────────────────────────────────────────────────────────── +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ProfileFunc { + name: String, + parent: i32, + canonical_id: u32, + kind: u32, + buffer_func_id: i32, + time_ns: u64, + memory_current: u64, + memory_peak: u64, + memory_total: u64, + stack_peak: u64, + active_threads_numerator: u32, + active_threads_denominator: u32, + num_allocs: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ProfilePipeline { + name: String, + runs: u32, + billed_runs: u32, + samples: u32, + num_allocs: u32, + time_ns: u64, + memory_current: u64, + memory_peak: u64, + memory_total: u64, + active_threads_numerator: u32, + active_threads_denominator: u32, + funcs: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Profile { + pipelines: Vec, +} + +impl Profile { + fn from_profile(path: &str) -> Result { + let data = std::fs::read(path).map_err(|e| e.to_string())?; + serde_json::from_slice(&data).map_err(|e| e.to_string()) + } +} + +#[tauri::command] +pub fn open_profile(path: &str) -> Result { + let profile = Profile::from_profile(path)?; + + Ok(profile) +} diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index fc07833ccdd9..ccc573cb9891 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -60,7 +60,8 @@ pub fn run() { commands::render_reuse_distance, commands::render_nan, commands::render_inf, - commands::render_thread + commands::render_thread, + commands::open_profile, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/apps/halidoscope/src-tauri/tauri.conf.json b/apps/halidoscope/src-tauri/tauri.conf.json index 0cb5107f9981..9cab6a6b6e73 100644 --- a/apps/halidoscope/src-tauri/tauri.conf.json +++ b/apps/halidoscope/src-tauri/tauri.conf.json @@ -40,7 +40,14 @@ "name": "trace", "short": "t", "takesValue": true, - "description": "Path to .hltrace file to load on startup." + "description": "Path to .hltrace file to load on startup.", + "required": true + }, + { + "name": "profile", + "short": "p", + "takesValue": true, + "description": "Path to Halide profile JSON file to load on startup." } ], "subcommands": { diff --git a/apps/halidoscope/src/App.css b/apps/halidoscope/src/App.css index 39e76d3be5e5..12e2cced0969 100644 --- a/apps/halidoscope/src/App.css +++ b/apps/halidoscope/src/App.css @@ -40,9 +40,11 @@ input[type="number"] { --color-ps-border-primary: oklch(0.3407 0 0); --color-ps-border-secondary: oklch(0.3979 0 0); --color-ps-border-tertiary: oklch(0.4997 0 0); - --color-realization: oklch(0.837 0.14 75); - --color-produce: oklch(0.77 0.1919 163.7); - --color-consume: oklch(0.74 0.175 305.4); + --color-oxide-green: oklch(0.77 0.1919 163.7); + --color-oxide-yellow: oklch(0.837 0.14 75); + --color-oxide-blue: oklch(0.71 0.15 272); + --color-oxide-red: oklch(0.712 0.185 11.3); + --color-oxide-purple: oklch(0.74 0.175 305.4); --text-tiny: 0.625rem; --text-tiny--line-height: 1.5; diff --git a/apps/halidoscope/src/App.tsx b/apps/halidoscope/src/App.tsx index af33ddb9b4ce..c7e74531e634 100644 --- a/apps/halidoscope/src/App.tsx +++ b/apps/halidoscope/src/App.tsx @@ -1,16 +1,25 @@ import { invoke } from "@tauri-apps/api/core"; import { getMatches } from "@tauri-apps/plugin-cli"; import { useSetAtom } from "jotai"; +import { Tabs } from "radix-ui"; import * as React from "react"; +import Profiler from "@/components/views/profiler/Profiler"; import Tracer from "@/components/views/tracer/Tracer"; +import { ProfileContextProvider } from "@/hooks/profile"; import { TraceContextProvider } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; -import type { FuncMeta, StatsMeta } from "@/types"; -import { openTrace } from "@/utils/api"; +import { Profile, type FuncMeta, type StatsMeta } from "@/types"; +import { openProfile, openTrace } from "@/utils/api"; import "./App.css"; +async function resolvePath(path: string) { + return path.startsWith("/") + ? path + : `${await invoke("get_cwd")}/${path}`; +} + function App() { const [funcs, setFuncs] = React.useState>({}); const [dagEdges, setDagEdges] = React.useState>({}); @@ -22,6 +31,7 @@ function App() { global_max_reuse_distance: 0, global_thread_ids: [], }); + const [profile, setProfile] = React.useState(null); const setActiveFunc = useSetAtom(funcAtom); @@ -33,14 +43,11 @@ function App() { if (typeof tracePath !== "string") { return; } - - const resolved = tracePath.startsWith("/") - ? tracePath - : `${await invoke("get_cwd")}/${tracePath}`; + const resolvedTracePath = await resolvePath(tracePath); try { const { funcs, total_packets, dag_edges, stats } = - await openTrace(resolved); + await openTrace(resolvedTracePath); const byName: Record = {}; for (const func of funcs) { @@ -60,19 +67,69 @@ function App() { loadTraceFromCLI(); }, [setActiveFunc]); + React.useEffect(() => { + async function loadProfileFromCLI() { + const matches = await getMatches(); + const profilePath = matches.args.profile?.value; + + if (typeof profilePath !== "string" || !profilePath) { + return; + } + const resolvedProfilePath = await resolvePath(profilePath); + + try { + const { pipelines } = await openProfile(resolvedProfilePath); + setProfile({ pipelines }); + } catch (err) { + console.error("Error loading profile from CLI: ", err); + } + } + + loadProfileFromCLI(); + }, []); + return ( - -
- -
-
+ + + + Trace + + {profile !== null ? ( + + Profile + + ) : null} + + + +
+ +
+
+
+ {profile !== null ? ( + + +
+ +
+
+
+ ) : null} +
); } diff --git a/apps/halidoscope/src/components/canvas/Canvas.tsx b/apps/halidoscope/src/components/canvas/Canvas.tsx index d2dee94821ae..b5550983faa5 100644 --- a/apps/halidoscope/src/components/canvas/Canvas.tsx +++ b/apps/halidoscope/src/components/canvas/Canvas.tsx @@ -80,22 +80,22 @@ function Canvas({ funcs, dagEdges }: CanvasProps) { {liveness.mode === "realizations" ? (
-
-
+
+
Buffer Live in Memory
) : (
-
-
+
+
Producer
-
-
+
+
Consumer
diff --git a/apps/halidoscope/src/components/canvas/FuncEdge.tsx b/apps/halidoscope/src/components/canvas/FuncEdge.tsx index 1457c27f26de..acc7a1719c4f 100644 --- a/apps/halidoscope/src/components/canvas/FuncEdge.tsx +++ b/apps/halidoscope/src/components/canvas/FuncEdge.tsx @@ -53,8 +53,8 @@ function FuncEdge({ x2={targetX} y2={targetY} > - - + + >) {
= 1, })} @@ -324,9 +324,9 @@ function FuncNode({ data }: NodeProps>) { width={width} height={height} className={clsx("ring-transparent", { - "ring-realization!": bufferLive, - "ring-produce!": producing, - "ring-consume!": consuming, + "ring-oxide-yellow!": bufferLive, + "ring-oxide-green!": producing, + "ring-oxide-purple!": consuming, "ring-2": zoom < 1, "ring-1": zoom >= 1, })} diff --git a/apps/halidoscope/src/components/controls/ControlTabs.tsx b/apps/halidoscope/src/components/controls/ControlTabs.tsx index 547bee754e9d..c25c0fcf636e 100644 --- a/apps/halidoscope/src/components/controls/ControlTabs.tsx +++ b/apps/halidoscope/src/components/controls/ControlTabs.tsx @@ -7,12 +7,12 @@ import { FuncMeta } from "@/types"; function ControlTabs({ funcs }: { funcs: Record }) { return ( -
+
+
+ +
+ +
+ ); +} + +export default Profiler; diff --git a/apps/halidoscope/src/components/views/profiler/Treemap.tsx b/apps/halidoscope/src/components/views/profiler/Treemap.tsx new file mode 100644 index 000000000000..7dba720c2b01 --- /dev/null +++ b/apps/halidoscope/src/components/views/profiler/Treemap.tsx @@ -0,0 +1,161 @@ +import { clsx } from "clsx"; +import * as d3 from "d3"; +import { Tooltip } from "radix-ui"; +import * as React from "react"; + +import { useProfileContext } from "@/hooks/profile"; +import type { Profile } from "@/types"; +import { profileMetricAtom, type ProfileMetric } from "@/state/profile-metric"; +import { useAtomValue } from "jotai"; + +type TreemapNode = { + name: string; + value?: number; + children?: TreemapNode[]; +}; + +function createFuncHierarchy( + profile: Profile, + metric: ProfileMetric, +): TreemapNode { + switch (metric) { + case "memory_peak": + case "memory_total": { + const bins: TreemapNode[] = [ + { name: "B", children: [] }, + { name: "KB", children: [] }, + { name: "MB", children: [] }, + { name: "GB", children: [] }, + ]; + const k = 1024; + + profile.pipelines[0].funcs.forEach((func) => { + const index = Math.floor(Math.log(func[metric]) / Math.log(k)); + + if (index >= 0 && index < bins.length) { + bins[index]?.children?.push({ name: func.name, value: func[metric] }); + } + }); + + return { + name: profile.pipelines[0].name, + children: bins, + }; + } + default: + return { + name: profile.pipelines[0].name, + children: profile.pipelines[0].funcs.map((func) => { + return { + name: func.name, + value: func[metric], + }; + }), + }; + } +} + +function formatBytes(d: number) { + return d3.format(".2s")(d).replace("k", "K").concat("B"); +} + +const METRIC_TO_FORMATTER: Record string> = { + memory_peak: formatBytes, + memory_total: formatBytes, + stack_peak: (d) => `${d}`, + num_allocs: (d) => `${d}`, +}; + +function Treemap() { + const profile = useProfileContext(); + const profileMetric = useAtomValue(profileMetricAtom); + + const root = React.useMemo(() => { + const hierarchy = d3 + .hierarchy(createFuncHierarchy(profile, profileMetric)) + .sum((d) => d.value ?? 0) + .sort((a, b) => (b.value ?? 0) - (a.value ?? 0)); + + const treemap = d3 + .treemap() + .tile(d3.treemapSquarify) + .size([960, 720]) + .padding(3) + .round(true); + + return treemap(hierarchy); + }, [profile, profileMetric]); + + return ( + + + {root.leaves().map((leaf, index) => ( + + + + + + {leaf.x1 - leaf.x0 > 25 || leaf.y1 - leaf.y0 > 25 ? ( + <> + + + + + + {leaf.data.name} + + + {METRIC_TO_FORMATTER[profileMetric](leaf.data.value ?? 0)} + + + + ) : null} + + + +

{leaf.data.name}

+

+ {METRIC_TO_FORMATTER[profileMetric](leaf.data.value ?? 0)} +

+ + + + + ))} + +
+ ); +} + +export default Treemap; diff --git a/apps/halidoscope/src/components/views/profiler/controls/ProfilerControlPanel.tsx b/apps/halidoscope/src/components/views/profiler/controls/ProfilerControlPanel.tsx new file mode 100644 index 000000000000..c7b190aba3ba --- /dev/null +++ b/apps/halidoscope/src/components/views/profiler/controls/ProfilerControlPanel.tsx @@ -0,0 +1,63 @@ +import { useAtom } from "jotai"; +import { snakeCase } from "lodash-es"; +import { Select } from "radix-ui"; + +import ControlSection from "@/components/controls/ControlSection"; +import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; +import { + PROFILE_METRICS, + type ProfileMetric, + profileMetricAtom, +} from "@/state/profile-metric"; + +function ProfilerControlPanel() { + const [profileMetric, setProfileMetric] = useAtom(profileMetricAtom); + + return ( +
+
+
+
+ + { + setProfileMetric(value as ProfileMetric); + }} + > + + + + + + + + + + + {PROFILE_METRICS.map((metric) => ( + + {metric} + + ))} + + + + +
+
+ ); +} + +export default ProfilerControlPanel; diff --git a/apps/halidoscope/src/components/views/tracer/Tracer.tsx b/apps/halidoscope/src/components/views/tracer/Tracer.tsx index 5526eb5e146a..6b5c415d5692 100644 --- a/apps/halidoscope/src/components/views/tracer/Tracer.tsx +++ b/apps/halidoscope/src/components/views/tracer/Tracer.tsx @@ -9,7 +9,7 @@ function Tracer() { const { funcs, dagEdges, packetCount } = useTraceContext(); return ( -
+
{Object.keys(funcs).length > 0 ? ( <> diff --git a/apps/halidoscope/src/hooks/profile.ts b/apps/halidoscope/src/hooks/profile.ts new file mode 100644 index 000000000000..3c32c19ef035 --- /dev/null +++ b/apps/halidoscope/src/hooks/profile.ts @@ -0,0 +1,10 @@ +import * as React from "react"; + +import type { Profile } from "@/types"; + +const Profile = React.createContext({ + pipelines: [], +}); + +export const ProfileContextProvider = Profile.Provider; +export const useProfileContext = () => React.useContext(Profile); diff --git a/apps/halidoscope/src/state/profile-metric.ts b/apps/halidoscope/src/state/profile-metric.ts new file mode 100644 index 000000000000..0d5e4a8bd81d --- /dev/null +++ b/apps/halidoscope/src/state/profile-metric.ts @@ -0,0 +1,16 @@ +import { atom } from "jotai"; + +export type ProfileMetric = + | "memory_peak" + | "memory_total" + | "stack_peak" + | "num_allocs"; + +export const PROFILE_METRICS = [ + "Memory Peak", + "Memory Total", + "Stack Peak", + "Num Allocs", +]; + +export const profileMetricAtom = atom("memory_total"); diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts index d6b3f4c90536..4e6d153f9034 100644 --- a/apps/halidoscope/src/types/index.ts +++ b/apps/halidoscope/src/types/index.ts @@ -43,3 +43,38 @@ export type NodeTypes = "funcNode"; export type EdgeTypes = "funcEdge"; export type AnimationMode = "Blink" | "Pulse" | "None"; + +export interface ProfileFunc { + name: string; + parent: number; + canonical_id: number; + kind: number; + buffer_func_id: number; + time_ns: number; + memory_current: number; + memory_peak: number; + memory_total: number; + stack_peak: number; + active_threads_numerator: number; + active_threads_denominator: number; + num_allocs: number; +} + +export interface ProfilePipeline { + name: string; + runs: number; + billed_runs: number; + samples: number; + num_allocs: number; + time_ns: number; + memory_current: number; + memory_peak: number; + memory_total: number; + active_threads_numerator: number; + active_threads_denominator: number; + funcs: ProfileFunc[]; +} + +export interface Profile { + pipelines: ProfilePipeline[]; +} diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index 8d8b4b4a512d..4ba8a9a5f397 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -2,7 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import type { NormalizationMode } from "@/state/render"; import type { ThreadOpMode } from "@/state/thread"; -import type { TraceMeta } from "@/types"; +import type { TraceMeta, Profile } from "@/types"; export async function openTrace(path: string): Promise { return invoke("open_trace", { path }); @@ -245,3 +245,7 @@ export async function renderThread({ includeTabularData, }); } + +export async function openProfile(path: string): Promise { + return invoke("open_profile", { path }); +} From bfcb8190f02ca66599536e0f6e585cda0c08b31d Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Mon, 27 Jul 2026 16:30:30 -0700 Subject: [PATCH 32/67] Reduce IPC calls by shifting NaN/Inf overlay computation into the IPC response for each render mode. Co-authored-by: Claude Opus 5 --- apps/halidoscope/src-tauri/src/commands.rs | 166 +++--- apps/halidoscope/src-tauri/src/lib.rs | 2 - apps/halidoscope/src-tauri/src/render.rs | 521 ++++++++---------- .../src/components/canvas/FuncNode.tsx | 113 +--- .../src/components/controls/ControlTabs.tsx | 2 +- apps/halidoscope/src/utils/api.ts | 166 +++--- 6 files changed, 418 insertions(+), 552 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index b68e3c321b41..d2f52843438b 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -10,8 +10,8 @@ use tauri::ipc::Response; use tauri::State; use crate::render::{ - GrayscaleState, InfState, LoadFrequencyState, NaNState, NormalizationMode, RedundantState, - Renderer, ReuseDistanceState, RgbState, StoreFrequencyState, ThreadOpMode, ThreadState, + GrayscaleState, LoadFrequencyState, NormalizationMode, RedundantState, Renderer, + ReuseDistanceState, RgbState, StoreFrequencyState, ThreadOpMode, ThreadState, }; use crate::trace::Trace; @@ -184,8 +184,6 @@ struct Loaded { load_frequency_renderers: HashMap, redundant_renderers: HashMap, reuse_distance_renderers: HashMap, - nan_renderers: HashMap, - inf_renderers: HashMap, thread_renderers: HashMap, } @@ -196,32 +194,18 @@ pub struct AppState { inner: Mutex>, } -/// Appends histogram data as little-endian `u32`s directly after `pixels`, so a single -/// `Response` carries both. The frontend already knows the pixel-buffer length ahead of time -/// (`width * height * 4`), so no length prefix is needed to split the two back apart. -fn pack_pixels_and_histogram(mut pixels: Vec, histogram: Vec) -> Vec { - pixels.reserve(histogram.len() * 4); - for bin in histogram { - pixels.extend_from_slice(&bin.to_le_bytes()); - } - - pixels -} - -/// Appends `store_counts` then `load_counts` as little-endian `u32`s directly after `pixels`. -/// Both slices are the same length (the Func's thread-ID domain size, `FuncMeta::thread_ids`), so -/// the frontend can split them back apart without a length prefix. -fn pack_pixels_and_thread_counts( +/// Packs tensor data, tabular data, and NaN / Inf data in a single IPC response. +fn pack_render_response( mut pixels: Vec, - store_counts: &[u32], - load_counts: &[u32], + nan_inf_overlays: Vec, + tabular_data: &[u32], ) -> Vec { - pixels.reserve((store_counts.len() + load_counts.len()) * 4); - for &c in store_counts { - pixels.extend_from_slice(&c.to_le_bytes()); - } - for &c in load_counts { - pixels.extend_from_slice(&c.to_le_bytes()); + pixels.reserve(nan_inf_overlays.len() + tabular_data.len() * 4); + + pixels.extend_from_slice(&nan_inf_overlays); + + for &v in tabular_data { + pixels.extend_from_slice(&v.to_le_bytes()); } pixels @@ -245,8 +229,6 @@ pub fn open_trace(path: String, state: State) -> Result, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -280,7 +264,13 @@ pub fn render_grayscale( let k = store_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, store_indices, k); - Ok(Response::new(renderer.to_rgba(normalization_mode))) + let pixels = renderer.to_rgba(normalization_mode); + let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( + pixels, + nan_inf_overlays, + &[], + ))) } /// Renders `func` as an RGB image at `global_index` and returns raw RGBA8 bytes. Planes 0/1/2 @@ -290,6 +280,8 @@ pub fn render_rgb( func: String, global_index: u32, normalization_mode: NormalizationMode, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -311,7 +303,13 @@ pub fn render_rgb( let k = store_indices.partition_point(|&p| p <= global_index as usize); renderer.seek(trace, store_indices, k); - Ok(Response::new(renderer.to_rgba(normalization_mode))) + let pixels = renderer.to_rgba(normalization_mode); + let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( + pixels, + nan_inf_overlays, + &[], + ))) } /// Renders a heatmap of store counts for `func` up to `global_index` and returns raw RGBA8 bytes. @@ -321,6 +319,8 @@ pub fn render_store_frequency( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -350,7 +350,12 @@ pub fn render_store_frequency( } else { Vec::new() }; - Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) + let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( + pixels, + nan_inf_overlays, + &histogram, + ))) } /// Renders a heatmap of load counts for `func` up to `global_index` and returns raw RGBA8 bytes. @@ -360,6 +365,8 @@ pub fn render_load_frequency( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -389,8 +396,13 @@ pub fn render_load_frequency( } else { Vec::new() }; + let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); - Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) + Ok(Response::new(pack_render_response( + pixels, + nan_inf_overlays, + &histogram, + ))) } /// Renders a heatmap of redundant store counts for `func` up to `global_index` and returns raw @@ -402,6 +414,8 @@ pub fn render_redundant_stores( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -429,7 +443,12 @@ pub fn render_redundant_stores( } else { Vec::new() }; - Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) + let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( + pixels, + nan_inf_overlays, + &histogram, + ))) } /// Renders a heatmap of maximum store-to-load reuse distances for `func` up to `global_index` @@ -441,6 +460,8 @@ pub fn render_reuse_distance( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -472,65 +493,12 @@ pub fn render_reuse_distance( } else { Vec::new() }; - Ok(Response::new(pack_pixels_and_histogram(pixels, histogram))) -} - -#[tauri::command] -pub fn render_nan( - func: String, - global_index: u32, - normalization_mode: NormalizationMode, - state: State, -) -> Result { - let mut guard = state.inner.lock().map_err(|e| e.to_string())?; - let loaded = guard.as_mut().ok_or("no trace loaded")?; - let Loaded { - trace, - nan_renderers, - .. - } = loaded; - - if !nan_renderers.contains_key(&func) { - let rs = NaNState::new(trace, &func) - .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; - nan_renderers.insert(func.clone(), rs); - } - let renderer = nan_renderers.get_mut(&func).expect("just inserted"); - - let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); - let k = store_indices.partition_point(|&p| p <= global_index as usize); - renderer.seek(trace, store_indices, k); - - Ok(Response::new(renderer.to_rgba(normalization_mode))) -} - -#[tauri::command] -pub fn render_inf( - func: String, - global_index: u32, - normalization_mode: NormalizationMode, - state: State, -) -> Result { - let mut guard = state.inner.lock().map_err(|e| e.to_string())?; - let loaded = guard.as_mut().ok_or("no trace loaded")?; - let Loaded { - trace, - inf_renderers, - .. - } = loaded; - - if !inf_renderers.contains_key(&func) { - let rs = InfState::new(trace, &func) - .ok_or_else(|| format!("func '{func}' has no renderable geometry"))?; - inf_renderers.insert(func.clone(), rs); - } - let renderer = inf_renderers.get_mut(&func).expect("just inserted"); - - let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); - let k = store_indices.partition_point(|&p| p <= global_index as usize); - renderer.seek(trace, store_indices, k); - - Ok(Response::new(renderer.to_rgba(normalization_mode))) + let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( + pixels, + nan_inf_overlays, + &histogram, + ))) } #[tauri::command] @@ -539,6 +507,8 @@ pub fn render_thread( global_index: u32, op_mode: ThreadOpMode, thread_id: String, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -563,10 +533,12 @@ pub fn render_thread( let pixels = renderer.to_rgba(thread_id); let (store_counts, load_counts) = renderer.to_thread_counts(); - Ok(Response::new(pack_pixels_and_thread_counts( + let thread_counts: Vec = store_counts.iter().chain(load_counts).copied().collect(); + let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( pixels, - store_counts, - load_counts, + nan_inf_overlays, + &thread_counts, ))) } diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index ccc573cb9891..c05c0de0dc14 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -58,8 +58,6 @@ pub fn run() { commands::render_load_frequency, commands::render_redundant_stores, commands::render_reuse_distance, - commands::render_nan, - commands::render_inf, commands::render_thread, commands::open_profile, ]) diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index c98a612abc98..7f290d65ff3c 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -3,7 +3,7 @@ use std::vec; use ::colorous; use serde::Deserialize; -use crate::trace::{for_each_lane_pixel, pixel_xy, FuncGeometry, Trace, TracePacket}; +use crate::trace::{for_each_lane_pixel, FuncGeometry, Trace, TracePacket}; #[derive(Deserialize, Clone, Copy)] pub enum NormalizationMode { @@ -17,12 +17,57 @@ pub enum NormalizationMode { pub trait Renderer: Sized { type Value; - fn register(trace: &Trace, func: &str) -> Option; fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize); fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec; + fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec; fn to_values(&self) -> Vec; } +/// Packs a NaN overlay (cyan) followed by an Inf overlay (yellow) into one RGBA8 buffer, +/// `width * height * 4` bytes each, from a renderer's per-`(pixel, channel)` decoded values. +fn nan_inf_overlay( + values: &[f64], + width: usize, + height: usize, + channels: usize, + include_nan: bool, + include_inf: bool, +) -> Vec { + let overlay_len = width * height * 4; + let mut out = vec![0u8; overlay_len * 2]; + let (nan_out, inf_out) = out.split_at_mut(overlay_len); + + if include_nan { + for (chunk, src) in nan_out + .chunks_exact_mut(4) + .zip(values.chunks_exact(channels)) + { + if src.iter().any(|v| v.is_nan()) { + chunk[0] = 0; + chunk[1] = 255; + chunk[2] = 255; + chunk[3] = 255; + } + } + } + + if include_inf { + for (chunk, src) in inf_out + .chunks_exact_mut(4) + .zip(values.chunks_exact(channels)) + { + if src.iter().any(|v| v.is_infinite()) { + chunk[0] = 255; + chunk[1] = 255; + chunk[2] = 0; + chunk[3] = 255; + } + } + } + + out +} + // ── Grayscale rendering ────────────────────────────────────────────────────────────────────────── pub struct GrayscaleState { @@ -72,12 +117,10 @@ impl GrayscaleState { height, Some((min_c, channels)), |lane, _pixel_idx, val_idx| { - let Some(v) = pkt.decoded_value(lane) else { - return; + if let Some(v) = pkt.decoded_value(lane) { + self.framebuffer[val_idx] = self.normalize(v); + self.values[val_idx] = v; }; - - self.framebuffer[val_idx] = self.normalize(v); - self.values[val_idx] = v; }, ); } @@ -91,19 +134,18 @@ impl GrayscaleState { impl Renderer for GrayscaleState { type Value = f64; - fn register(trace: &Trace, func: &str) -> Option { - Self::new(trace, func) - } - fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { let target_k = target_k.min(store_indices.len()); if target_k < self.applied_k { self.framebuffer.iter_mut().for_each(|b| *b = 0); + self.values.iter_mut().for_each(|v| *v = 0.0); self.applied_k = 0; } + for &global_idx in &store_indices[self.applied_k..target_k] { self.apply_store(&trace.packets[global_idx]); } + self.applied_k = target_k; } @@ -134,12 +176,31 @@ impl Renderer for GrayscaleState { chunk[3] = 255; } } + out } fn to_values(&self) -> Vec { self.values.clone() } + + fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + + nan_inf_overlay( + &self.values, + width, + height, + channels, + include_nan, + include_inf, + ) + } } // ── RGB rendering ──────────────────────────────────────────────────────────────────────────────── @@ -191,12 +252,10 @@ impl RgbState { height, Some((min_c, channels)), |lane, _pixel_idx, val_idx| { - let Some(v) = pkt.decoded_value(lane) else { - return; + if let Some(v) = pkt.decoded_value(lane) { + self.framebuffer[val_idx] = self.normalize(v); + self.values[val_idx] = v; }; - - self.framebuffer[val_idx] = self.normalize(v); - self.values[val_idx] = v; }, ); } @@ -210,19 +269,18 @@ impl RgbState { impl Renderer for RgbState { type Value = f64; - fn register(trace: &Trace, func: &str) -> Option { - Self::new(trace, func) - } - fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { let target_k = target_k.min(store_indices.len()); if target_k < self.applied_k { self.framebuffer.iter_mut().for_each(|b| *b = 0); + self.values.iter_mut().for_each(|v| *v = 0.0); self.applied_k = 0; } + for &global_idx in &store_indices[self.applied_k..target_k] { self.apply_store(&trace.packets[global_idx]); } + self.applied_k = target_k; } @@ -250,12 +308,31 @@ impl Renderer for RgbState { chunk[3] = 255; } } + out } fn to_values(&self) -> Vec { self.values.clone() } + + fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + + nan_inf_overlay( + &self.values, + width, + height, + channels, + include_nan, + include_inf, + ) + } } // ── Store frequency rendering ──────────────────────────────────────────────────────────────────── @@ -263,6 +340,7 @@ impl Renderer for RgbState { pub struct StoreFrequencyState { geom: FuncGeometry, counts: Vec, + values: Vec, local_max_store_count: u32, global_max_store_count: u32, applied_k: usize, @@ -272,6 +350,7 @@ impl StoreFrequencyState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let counts = vec![0u32; geom.width * geom.height]; + let values = vec![0f64; geom.width * geom.height * geom.channels]; let local_max_store_count = trace.funcs.get(func).map_or(0, |s| s.max_store_count); let global_max_store_count = trace @@ -284,6 +363,7 @@ impl StoreFrequencyState { Some(Self { geom, counts, + values, local_max_store_count, global_max_store_count, applied_k: 0, @@ -294,19 +374,28 @@ impl StoreFrequencyState { let FuncGeometry { width, height, + channels, min_x, min_y, + min_c, .. } = self.geom; - let n_lanes = pkt.type_.lanes.max(1) as usize; - let dims_per_lane = pkt.coordinates.len() / n_lanes; - for l in 0..n_lanes { - let (x, y) = pixel_xy(pkt, l, n_lanes, dims_per_lane, min_x, min_y); - if x >= 0 && y >= 0 && (x as usize) < width && (y as usize) < height { - self.counts[y as usize * width + x as usize] += 1; - } - } + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + Some((min_c, channels)), + |lane, pixel_idx, val_idx| { + self.counts[pixel_idx] += 1; + + if let Some(v) = pkt.decoded_value(lane) { + self.values[val_idx] = v; + } + }, + ); } pub fn to_tabular_data(&self, normalization_mode: NormalizationMode) -> Vec { @@ -339,14 +428,11 @@ impl StoreFrequencyState { impl Renderer for StoreFrequencyState { type Value = u32; - fn register(trace: &Trace, func: &str) -> Option { - Self::new(trace, func) - } - fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { let target_k = target_k.min(store_indices.len()); if target_k < self.applied_k { self.counts.iter_mut().for_each(|c| *c = 0); + self.values.iter_mut().for_each(|v| *v = 0.0); self.applied_k = 0; } for &idx in &store_indices[self.applied_k..target_k] { @@ -391,6 +477,24 @@ impl Renderer for StoreFrequencyState { fn to_values(&self) -> Vec { self.counts.clone() } + + fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + + nan_inf_overlay( + &self.values, + width, + height, + channels, + include_nan, + include_inf, + ) + } } // ── Load frequency rendering ───────────────────────────────────────────────────────────────────── @@ -398,6 +502,7 @@ impl Renderer for StoreFrequencyState { pub struct LoadFrequencyState { geom: FuncGeometry, counts: Vec, + values: Vec, local_max_load_count: u32, global_max_load_count: u32, applied_k: usize, @@ -407,6 +512,7 @@ impl LoadFrequencyState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let counts = vec![0u32; geom.width * geom.height]; + let values = vec![0f64; geom.width * geom.height * geom.channels]; let local_max_load_count = trace.funcs.get(func).map_or(0, |s| s.max_load_count); let global_max_load_count = trace @@ -419,6 +525,7 @@ impl LoadFrequencyState { Some(Self { geom, counts, + values, local_max_load_count, global_max_load_count, applied_k: 0, @@ -429,8 +536,10 @@ impl LoadFrequencyState { let FuncGeometry { width, height, + channels, min_x, min_y, + min_c, .. } = self.geom; @@ -440,9 +549,13 @@ impl LoadFrequencyState { min_y, width, height, - None, - |_lane, pixel_idx, _val_idx| { + Some((min_c, channels)), + |lane, pixel_idx, val_idx| { self.counts[pixel_idx] += 1; + + if let Some(v) = pkt.decoded_value(lane) { + self.values[val_idx] = v; + } }, ); } @@ -477,14 +590,11 @@ impl LoadFrequencyState { impl Renderer for LoadFrequencyState { type Value = u32; - fn register(trace: &Trace, func: &str) -> Option { - Self::new(trace, func) - } - fn seek(&mut self, trace: &Trace, load_indices: &[usize], target_k: usize) { let target_k = target_k.min(load_indices.len()); if target_k < self.applied_k { self.counts.iter_mut().for_each(|c| *c = 0); + self.values.iter_mut().for_each(|v| *v = 0.0); self.applied_k = 0; } for &idx in &load_indices[self.applied_k..target_k] { @@ -528,6 +638,24 @@ impl Renderer for LoadFrequencyState { fn to_values(&self) -> Vec { self.counts.clone() } + + fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + + nan_inf_overlay( + &self.values, + width, + height, + channels, + include_nan, + include_inf, + ) + } } // ── Redundant store rendering ──────────────────────────────────────────────────────────────────── @@ -635,10 +763,6 @@ impl RedundantState { impl Renderer for RedundantState { type Value = u32; - fn register(trace: &Trace, func: &str) -> Option { - Self::new(trace, func) - } - fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { let target_k = target_k.min(store_indices.len()); if target_k < self.applied_k { @@ -691,6 +815,22 @@ impl Renderer for RedundantState { fn to_values(&self) -> Vec { self.redundant_store_counts.clone() } + + fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + let values: Vec = self + .last_values + .iter() + .map(|v| v.map_or(0.0, f64::from_bits)) + .collect(); + + nan_inf_overlay(&values, width, height, channels, include_nan, include_inf) + } } // ── Reuse distance rendering ───────────────────────────────────────────────────────────────────── @@ -718,6 +858,7 @@ pub struct ReuseDistanceState { global_max_reuse_distance: u64, applied_store_k: usize, applied_load_k: usize, + values: Vec, } impl ReuseDistanceState { @@ -740,12 +881,14 @@ impl ReuseDistanceState { global_max_reuse_distance, applied_store_k: 0, applied_load_k: 0, + values: vec![0f64; n_cells], }) } fn reset(&mut self) { self.anchor_at.iter_mut().for_each(|v| *v = usize::MAX); self.max_reuse_distance.iter_mut().for_each(|d| *d = 0); + self.values.iter_mut().for_each(|v| *v = 0.0); self.applied_store_k = 0; self.applied_load_k = 0; } @@ -813,8 +956,12 @@ impl ReuseDistanceState { width, height, Some((min_c, channels)), - |_lane, _pixel_idx, val_idx| { + |lane, _pixel_idx, val_idx| { self.anchor_at[val_idx] = global_idx; + + if let Some(v) = pkt.decoded_value(lane) { + self.values[val_idx] = v; + } }, ); } @@ -911,256 +1058,26 @@ impl ReuseDistanceState { tabular_data } - /// Returns the per-pixel maximum reuse distance at the current seek position, in row-major - /// `(y * width + x)` order. pub fn to_values(&self) -> Vec { self.max_reuse_distance.clone() } -} - -// ── NaN Rendering ──────────────────────────────────────────────────────────────────────────────── -pub struct NaNState { - geom: FuncGeometry, - values: Vec, - nanbuffer: Vec, - applied_k: usize, -} - -impl NaNState { - pub fn new(trace: &Trace, func: &str) -> Option { - let geom = trace.func_geometry(func)?; - let nanbuffer = vec![0u8; geom.width * geom.height * geom.channels]; - let values = vec![0f64; geom.width * geom.height * geom.channels]; - - Some(Self { - geom, - values, - nanbuffer, - applied_k: 0, - }) - } - - fn apply_store(&mut self, pkt: &TracePacket) { + pub fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { let FuncGeometry { width, height, channels, - min_x, - min_y, - min_c, .. } = self.geom; - for_each_lane_pixel( - pkt, - min_x, - min_y, - width, - height, - Some((min_c, channels)), - |lane, _pixel_idx: usize, val_idx: usize| { - let Some(v) = pkt.decoded_value(lane) else { - return; - }; - - self.nanbuffer[val_idx] = if v.is_nan() { 1 } else { 0 }; - self.values[val_idx] = v; - }, - ); - } -} - -impl Renderer for NaNState { - type Value = f64; - - fn register(trace: &Trace, func: &str) -> Option { - Self::new(trace, func) - } - - fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { - let target_k = target_k.min(store_indices.len()); - if target_k < self.applied_k { - self.values.iter_mut().for_each(|v| *v = 0.0); - self.nanbuffer.iter_mut().for_each(|b| *b = 0); - self.applied_k = 0; - } - - for &idx in &store_indices[self.applied_k..target_k] { - self.apply_store(&trace.packets[idx]); - } - - self.applied_k = target_k; - } - - fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - let mut out = vec![0u8; width * height * 4]; - let nb = &self.nanbuffer; - if channels >= 3 { - for (chunk, src) in out.chunks_exact_mut(4).zip(nb.chunks_exact(channels)) { - // If any channel is NaN, mark the pixel as cyan, otherwise transparent. - if src[0] == 1 || src[1] == 1 || src[2] == 1 { - chunk[0] = 0; - chunk[1] = 255; - chunk[2] = 255; - chunk[3] = 255; - } else { - chunk[0] = 0; - chunk[1] = 0; - chunk[2] = 0; - chunk[3] = 0; - } - } - } else { - for (chunk, src) in out.chunks_exact_mut(4).zip(nb.chunks_exact(channels)) { - // If the channel is NaN, mark the pixel as cyan, otherwise transparent. - if src[0] == 1 { - chunk[0] = 0; - chunk[1] = 255; - chunk[2] = 255; - chunk[3] = 255; - } else { - chunk[0] = 0; - chunk[1] = 0; - chunk[2] = 0; - chunk[3] = 0; - } - } - } - out - } - - fn to_values(&self) -> Vec { - self.values.clone() - } -} - -// ── Inf Rendering ──────────────────────────────────────────────────────────────────────────────── - -pub struct InfState { - geom: FuncGeometry, - values: Vec, - infbuffer: Vec, - applied_k: usize, -} - -impl InfState { - pub fn new(trace: &Trace, func: &str) -> Option { - let geom = trace.func_geometry(func)?; - let infbuffer = vec![0u8; geom.width * geom.height * geom.channels]; - let values = vec![0f64; geom.width * geom.height * geom.channels]; - - Some(Self { - geom, - values, - infbuffer, - applied_k: 0, - }) - } - - fn apply_store(&mut self, pkt: &TracePacket) { - let FuncGeometry { - width, - height, - channels, - min_x, - min_y, - min_c, - .. - } = self.geom; - - for_each_lane_pixel( - pkt, - min_x, - min_y, - width, - height, - Some((min_c, channels)), - |lane, _pixel_idx: usize, val_idx: usize| { - let Some(v) = pkt.decoded_value(lane) else { - return; - }; - - self.infbuffer[val_idx] = if v.is_infinite() { 1 } else { 0 }; - self.values[val_idx] = v; - }, - ); - } -} - -impl Renderer for InfState { - type Value = f64; - - fn register(trace: &Trace, func: &str) -> Option { - Self::new(trace, func) - } - - fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { - let target_k = target_k.min(store_indices.len()); - if target_k < self.applied_k { - self.values.iter_mut().for_each(|v| *v = 0.0); - self.infbuffer.iter_mut().for_each(|b| *b = 0); - self.applied_k = 0; - } - - for &idx in &store_indices[self.applied_k..target_k] { - self.apply_store(&trace.packets[idx]); - } - - self.applied_k = target_k; - } - - fn to_rgba(&self, _normalization_mode: NormalizationMode) -> Vec { - let FuncGeometry { + nan_inf_overlay( + &self.values, width, height, channels, - .. - } = self.geom; - let mut out = vec![0u8; width * height * 4]; - let ib = &self.infbuffer; - if channels >= 3 { - for (chunk, src) in out.chunks_exact_mut(4).zip(ib.chunks_exact(channels)) { - // If any channel is inf, mark the pixel as magenta, otherwise transparent. - if src[0] == 1 || src[1] == 1 || src[2] == 1 { - chunk[0] = 255; - chunk[1] = 255; - chunk[2] = 0; - chunk[3] = 255; - } else { - chunk[0] = 0; - chunk[1] = 0; - chunk[2] = 0; - chunk[3] = 0; - } - } - } else { - for (chunk, src) in out.chunks_exact_mut(4).zip(ib.chunks_exact(channels)) { - // If the channel is inf, mark the pixel as magenta, otherwise transparent. - if src[0] == 1 { - chunk[0] = 255; - chunk[1] = 255; - chunk[2] = 0; - chunk[3] = 255; - } else { - chunk[0] = 0; - chunk[1] = 0; - chunk[2] = 0; - chunk[3] = 0; - } - } - } - out - } - - fn to_values(&self) -> Vec { - self.values.clone() + include_nan, + include_inf, + ) } } @@ -1182,6 +1099,7 @@ pub struct ThreadState { applied_store_k: usize, applied_load_k: usize, applied_op_mode: Option, + values: Vec, } impl ThreadState { @@ -1215,6 +1133,7 @@ impl ThreadState { applied_store_k: 0, applied_load_k: 0, applied_op_mode: None, + values: vec![0f64; geom.width * geom.height * geom.channels], }) } @@ -1222,6 +1141,7 @@ impl ThreadState { self.thread_id_buffer.iter_mut().for_each(|v| *v = -1); self.store_counts.iter_mut().for_each(|c| *c = 0); self.load_counts.iter_mut().for_each(|c| *c = 0); + self.values.iter_mut().for_each(|v| *v = 0.0); self.applied_store_k = 0; self.applied_load_k = 0; } @@ -1230,8 +1150,10 @@ impl ThreadState { let FuncGeometry { width, height, + channels, min_x, min_y, + min_c, .. } = self.geom; let thread_idx = self.thread_ids.binary_search(&pkt.thread_id).ok(); @@ -1242,10 +1164,10 @@ impl ThreadState { min_y, width, height, - None, - |lane, pixel_idx: usize, _val_idx: usize| { - let Some(_v) = pkt.decoded_value(lane) else { - return; + Some((min_c, channels)), + |lane, pixel_idx: usize, val_idx: usize| { + if let Some(v) = pkt.decoded_value(lane) { + self.values[val_idx] = v; }; self.thread_id_buffer[pixel_idx] = pkt.thread_id; @@ -1260,8 +1182,10 @@ impl ThreadState { let FuncGeometry { width, height, + channels, min_x, min_y, + min_c, .. } = self.geom; let thread_idx = self.thread_ids.binary_search(&pkt.thread_id).ok(); @@ -1272,10 +1196,10 @@ impl ThreadState { min_y, width, height, - None, - |lane, pixel_idx: usize, _val_idx: usize| { - let Some(_v) = pkt.decoded_value(lane) else { - return; + Some((min_c, channels)), + |lane, pixel_idx: usize, val_idx: usize| { + if let Some(v) = pkt.decoded_value(lane) { + self.values[val_idx] = v; }; self.thread_id_buffer[pixel_idx] = pkt.thread_id; @@ -1316,11 +1240,13 @@ impl ThreadState { match (&op_mode, next_is_store) { (ThreadOpMode::Store, true) => { - self.apply_store(&trace.packets[store_slice[si]]); + let pkt = &trace.packets[store_slice[si]]; + self.apply_store(pkt); si += 1; } (ThreadOpMode::Load, false) => { - self.apply_load(&trace.packets[load_slice[li]]); + let pkt = &trace.packets[load_slice[li]]; + self.apply_load(pkt); li += 1; } (_, true) => { @@ -1379,4 +1305,21 @@ impl ThreadState { pub fn to_thread_counts(&self) -> (&[u32], &[u32]) { (&self.store_counts, &self.load_counts) } + + pub fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + nan_inf_overlay( + &self.values, + width, + height, + channels, + include_nan, + include_inf, + ) + } } diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 0a1de6394fc6..21f8d97234e8 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -32,8 +32,6 @@ import { renderLoadFrequency, renderRedundantStores, renderReuseDistance, - renderNaN, - renderInf, renderThread, type RenderFuncParams, type RenderFuncResponse, @@ -127,6 +125,8 @@ function FuncNode({ data }: NodeProps>) { width, height, includeTabularData: active, + includeNan: nan.active, + includeInf: inf.active, }; switch (render.renderMode) { @@ -167,6 +167,24 @@ function FuncNode({ data }: NodeProps>) { ); } + const nanCtx = nanOverlayRef.current?.getContext("2d"); + if (nanCtx) { + nanCtx.putImageData( + new ImageData(result.nanOverlayData, width, height), + 0, + 0, + ); + } + + const infCtx = infOverlayRef.current?.getContext("2d"); + if (infCtx) { + infCtx.putImageData( + new ImageData(result.infOverlayData, width, height), + 0, + 0, + ); + } + // Update the histogram data for the currently active Func. if (active) { setTabularData((prev) => ({ @@ -197,97 +215,10 @@ function FuncNode({ data }: NodeProps>) { activeFunc, setTabularData, thread, + nan.active, + inf.active, ]); - React.useEffect(() => { - latestIndexRef.current = packetIndex; - - if (!nan.active || renderingRef.current) { - return; - } - - async function drawNaN() { - try { - while (true) { - const target = latestIndexRef.current; - - const result = await renderNaN({ - func: name, - globalIndex: packetIndex, - normalizationMode: render.normalizationMode, - width, - height, - includeTabularData: false, - }); - - const ctx = nanOverlayRef.current?.getContext("2d"); - - if (ctx) { - ctx.putImageData( - new ImageData(result.tensorData, width, height), - 0, - 0, - ); - } - - if (latestIndexRef.current === target) { - break; - } - } - } catch { - console.error( - `Failed to render NaN overlay for ${name} at index ${latestIndexRef.current}`, - ); - } - } - - drawNaN(); - }, [nan.active, name, packetIndex, render.normalizationMode, width, height]); - - React.useEffect(() => { - latestIndexRef.current = packetIndex; - - if (!inf.active || renderingRef.current) { - return; - } - - async function drawInf() { - try { - while (true) { - const target = latestIndexRef.current; - - const result = await renderInf({ - func: name, - globalIndex: packetIndex, - normalizationMode: render.normalizationMode, - width, - height, - includeTabularData: false, - }); - - const ctx = infOverlayRef.current?.getContext("2d"); - if (ctx) { - ctx.putImageData( - new ImageData(result.tensorData, width, height), - 0, - 0, - ); - } - - if (latestIndexRef.current === target) { - break; - } - } - } catch { - console.error( - `Failed to render Inf overlay for ${name} at index ${latestIndexRef.current}`, - ); - } - } - - drawInf(); - }, [inf.active, name, packetIndex, render.normalizationMode, width, height]); - return ( <> }) { - + diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index 4ba8a9a5f397..12a861daff30 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -10,6 +10,8 @@ export async function openTrace(path: string): Promise { export interface RenderFuncResponse { tensorData: Uint8ClampedArray; + nanOverlayData: Uint8ClampedArray; + infOverlayData: Uint8ClampedArray; tabularData: Uint32Array | null; } @@ -20,55 +22,22 @@ export interface RenderFuncParams { width: number; height: number; includeTabularData: boolean; -} - -export async function renderGrayscale({ - func, - globalIndex, - normalizationMode, -}: RenderFuncParams): Promise { - const buffer = await invoke("render_grayscale", { - func, - globalIndex, - normalizationMode, - }); - - return { - tensorData: new Uint8ClampedArray(buffer), - tabularData: null, - }; -} - -export async function renderRgb({ - func, - globalIndex, - normalizationMode, -}: RenderFuncParams): Promise { - const buffer = await invoke("render_rgb", { - func, - globalIndex, - normalizationMode, - }); - - return { - tensorData: new Uint8ClampedArray(buffer), - tabularData: null, - }; + includeNan: boolean; + includeInf: boolean; } /** - * Split the ArrayBuffer returned by {@link RenderFuncResponse} into the tensor - * data and the (optionally returned) tabular data. We only include tabular - * data for the actively selected Func. + * Splits the ArrayBuffer returned by a render command into the tensor data, + * NaN/Inf overlays, and the (optionally returned) tabular data. * - * @param buffer The buffer containing tensor data. + * @param buffer The buffer containing tensor, tabular, and overlay data. * @param width The width of the buffer. * @param height The height of the buffer. - * @param includeTabularData A flag indicating whether of not to expect tabular + * @param includeTabularData A flag indicating whether or not to expect tabular * data in the buffer payload. * @returns A {@link RenderFuncResponse}. */ -function splitTensorDataAndTabularData({ +function splitRenderBuffer({ buffer, width, height, @@ -80,77 +49,102 @@ function splitTensorDataAndTabularData({ includeTabularData: boolean; }): RenderFuncResponse { const pixelByteLength = width * height * 4; + const overlayPlaneByteLength = width * height * 4; + const tabularByteLength = + buffer.byteLength - pixelByteLength - overlayPlaneByteLength * 2; return { tensorData: new Uint8ClampedArray(buffer, 0, pixelByteLength), + nanOverlayData: new Uint8ClampedArray( + buffer, + pixelByteLength, + overlayPlaneByteLength, + ), + infOverlayData: new Uint8ClampedArray( + buffer, + pixelByteLength + overlayPlaneByteLength, + overlayPlaneByteLength, + ), tabularData: includeTabularData - ? new Uint32Array(buffer, pixelByteLength) + ? new Uint32Array( + buffer, + pixelByteLength + 2 * overlayPlaneByteLength, + tabularByteLength / 4, + ) : null, }; } -export async function renderStoreFrequency({ +export async function renderGrayscale({ func, globalIndex, normalizationMode, width, height, - includeTabularData, + includeNan, + includeInf, }: RenderFuncParams): Promise { - const buffer = await invoke("render_store_frequency", { + const buffer = await invoke("render_grayscale", { func, globalIndex, normalizationMode, - includeTabularData, + includeNan, + includeInf, }); - return splitTensorDataAndTabularData({ + return splitRenderBuffer({ buffer, width, height, - includeTabularData, + includeTabularData: false, }); } -export async function renderLoadFrequency({ +export async function renderRgb({ func, globalIndex, normalizationMode, width, height, - includeTabularData, + includeNan, + includeInf, }: RenderFuncParams): Promise { - const buffer = await invoke("render_load_frequency", { + const buffer = await invoke("render_rgb", { func, globalIndex, normalizationMode, - includeTabularData, + includeNan, + includeInf, }); - return splitTensorDataAndTabularData({ + return splitRenderBuffer({ buffer, width, height, - includeTabularData, + includeTabularData: false, }); } -export async function renderRedundantStores({ +export async function renderStoreFrequency({ func, globalIndex, normalizationMode, width, height, includeTabularData, + includeNan, + includeInf, }: RenderFuncParams): Promise { - const buffer = await invoke("render_redundant_stores", { + const buffer = await invoke("render_store_frequency", { func, globalIndex, normalizationMode, includeTabularData, + includeNan, + includeInf, }); - return splitTensorDataAndTabularData({ + return splitRenderBuffer({ buffer, width, height, @@ -158,22 +152,26 @@ export async function renderRedundantStores({ }); } -export async function renderReuseDistance({ +export async function renderLoadFrequency({ func, globalIndex, normalizationMode, width, height, includeTabularData, + includeNan, + includeInf, }: RenderFuncParams): Promise { - const buffer = await invoke("render_reuse_distance", { + const buffer = await invoke("render_load_frequency", { func, globalIndex, normalizationMode, includeTabularData, + includeNan, + includeInf, }); - return splitTensorDataAndTabularData({ + return splitRenderBuffer({ buffer, width, height, @@ -181,38 +179,58 @@ export async function renderReuseDistance({ }); } -export async function renderNaN({ +export async function renderRedundantStores({ func, globalIndex, normalizationMode, + width, + height, + includeTabularData, + includeNan, + includeInf, }: RenderFuncParams): Promise { - const buffer = await invoke("render_nan", { + const buffer = await invoke("render_redundant_stores", { func, globalIndex, normalizationMode, + includeTabularData, + includeNan, + includeInf, }); - return { - tensorData: new Uint8ClampedArray(buffer), - tabularData: null, - }; + return splitRenderBuffer({ + buffer, + width, + height, + includeTabularData, + }); } -export async function renderInf({ +export async function renderReuseDistance({ func, globalIndex, normalizationMode, + width, + height, + includeTabularData, + includeNan, + includeInf, }: RenderFuncParams): Promise { - const buffer = await invoke("render_inf", { + const buffer = await invoke("render_reuse_distance", { func, globalIndex, normalizationMode, + includeTabularData, + includeNan, + includeInf, }); - return { - tensorData: new Uint8ClampedArray(buffer), - tabularData: null, - }; + return splitRenderBuffer({ + buffer, + width, + height, + includeTabularData, + }); } export interface RenderThreadFuncParams extends RenderFuncParams { @@ -229,6 +247,8 @@ export async function renderThread({ width, height, includeTabularData, + includeNan, + includeInf, }: RenderThreadFuncParams): Promise { const buffer = await invoke("render_thread", { func, @@ -236,9 +256,11 @@ export async function renderThread({ normalizationMode, opMode: threadOpMode, threadId: threadId, + includeNan, + includeInf, }); - return splitTensorDataAndTabularData({ + return splitRenderBuffer({ buffer, width, height, From a598fde6b11d666b488a82e182a0f3e13f12f4d7 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 28 Jul 2026 15:35:19 -0700 Subject: [PATCH 33/67] Support user-selected colors for NaN and Inf highlighting. Rework UI to keep display parameters visible across all top-level tabs. --- apps/halidoscope/src-tauri/src/commands.rs | 32 ++--- apps/halidoscope/src-tauri/src/render.rs | 69 ++++++---- apps/halidoscope/src/App.css | 48 +++++-- .../src/components/canvas/FuncNode.tsx | 14 ++- .../src/components/controls/ControlTabs.tsx | 89 +++++++------ .../src/components/controls/DebugPanel.tsx | 6 + .../src/components/controls/DisplayPanel.tsx | 21 ++++ .../controls/VisualizationPanel.tsx | 14 --- .../components/controls/color/ColorInput.tsx | 118 ++++++++++++++++++ .../components/controls/inf/InfControls.tsx | 102 +++++++++------ .../components/controls/nan/NaNControls.tsx | 102 +++++++++------ apps/halidoscope/src/state/inf.ts | 20 ++- apps/halidoscope/src/state/nan.ts | 20 ++- apps/halidoscope/src/utils/api.ts | 16 ++- 14 files changed, 486 insertions(+), 185 deletions(-) create mode 100644 apps/halidoscope/src/components/controls/DisplayPanel.tsx create mode 100644 apps/halidoscope/src/components/controls/color/ColorInput.tsx diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index d2f52843438b..0024d14b1338 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -10,8 +10,8 @@ use tauri::ipc::Response; use tauri::State; use crate::render::{ - GrayscaleState, LoadFrequencyState, NormalizationMode, RedundantState, Renderer, - ReuseDistanceState, RgbState, StoreFrequencyState, ThreadOpMode, ThreadState, + GrayscaleState, IncludeInf, IncludeNan, LoadFrequencyState, NormalizationMode, RedundantState, + Renderer, ReuseDistanceState, RgbState, StoreFrequencyState, ThreadOpMode, ThreadState, }; use crate::trace::Trace; @@ -241,8 +241,8 @@ pub fn render_grayscale( func: String, global_index: u32, normalization_mode: NormalizationMode, - include_nan: bool, - include_inf: bool, + include_nan: IncludeNan, + include_inf: IncludeInf, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -280,8 +280,8 @@ pub fn render_rgb( func: String, global_index: u32, normalization_mode: NormalizationMode, - include_nan: bool, - include_inf: bool, + include_nan: IncludeNan, + include_inf: IncludeInf, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -319,8 +319,8 @@ pub fn render_store_frequency( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: bool, - include_inf: bool, + include_nan: IncludeNan, + include_inf: IncludeInf, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -365,8 +365,8 @@ pub fn render_load_frequency( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: bool, - include_inf: bool, + include_nan: IncludeNan, + include_inf: IncludeInf, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -414,8 +414,8 @@ pub fn render_redundant_stores( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: bool, - include_inf: bool, + include_nan: IncludeNan, + include_inf: IncludeInf, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -460,8 +460,8 @@ pub fn render_reuse_distance( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: bool, - include_inf: bool, + include_nan: IncludeNan, + include_inf: IncludeInf, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -507,8 +507,8 @@ pub fn render_thread( global_index: u32, op_mode: ThreadOpMode, thread_id: String, - include_nan: bool, - include_inf: bool, + include_nan: IncludeNan, + include_inf: IncludeInf, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index 7f290d65ff3c..fa2bf1f22c3f 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -13,54 +13,79 @@ pub enum NormalizationMode { PerFunc, } +#[derive(Deserialize, Clone, Copy)] +pub struct IncludeNan { + pub active: bool, + pub r: u8, + pub g: u8, + pub b: u8, + pub a: f64, +} + +/// The frontend's Inf overlay toggle and color, `infAtom` in `state/inf.ts`. `r`/`g`/`b` are +/// 8-bit channels; `a` is a [0, 1] fraction converted to an 8-bit alpha when painting the overlay. +#[derive(Deserialize, Clone, Copy)] +pub struct IncludeInf { + pub active: bool, + pub r: u8, + pub g: u8, + pub b: u8, + pub a: f64, +} + // A trait that all 2D Canvas renderers implement. pub trait Renderer: Sized { type Value; fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize); fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec; - fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec; + fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec; fn to_values(&self) -> Vec; } -/// Packs a NaN overlay (cyan) followed by an Inf overlay (yellow) into one RGBA8 buffer, -/// `width * height * 4` bytes each, from a renderer's per-`(pixel, channel)` decoded values. +/// Packs a NaN overlay (colored per `include_nan`) followed by an Inf overlay (colored per +/// `include_inf`) into one RGBA8 buffer, `width * height * 4` bytes each, from a renderer's +/// per-`(pixel, channel)` decoded values. fn nan_inf_overlay( values: &[f64], width: usize, height: usize, channels: usize, - include_nan: bool, - include_inf: bool, + include_nan: IncludeNan, + include_inf: IncludeInf, ) -> Vec { let overlay_len = width * height * 4; let mut out = vec![0u8; overlay_len * 2]; let (nan_out, inf_out) = out.split_at_mut(overlay_len); - if include_nan { + if include_nan.active { + let alpha = (include_nan.a * 255.0).clamp(0.0, 255.0) as u8; + for (chunk, src) in nan_out .chunks_exact_mut(4) .zip(values.chunks_exact(channels)) { if src.iter().any(|v| v.is_nan()) { - chunk[0] = 0; - chunk[1] = 255; - chunk[2] = 255; - chunk[3] = 255; + chunk[0] = include_nan.r; + chunk[1] = include_nan.g; + chunk[2] = include_nan.b; + chunk[3] = alpha; } } } - if include_inf { + if include_inf.active { + let alpha = (include_inf.a * 255.0).clamp(0.0, 255.0) as u8; + for (chunk, src) in inf_out .chunks_exact_mut(4) .zip(values.chunks_exact(channels)) { if src.iter().any(|v| v.is_infinite()) { - chunk[0] = 255; - chunk[1] = 255; - chunk[2] = 0; - chunk[3] = 255; + chunk[0] = include_inf.r; + chunk[1] = include_inf.g; + chunk[2] = include_inf.b; + chunk[3] = alpha; } } } @@ -184,7 +209,7 @@ impl Renderer for GrayscaleState { self.values.clone() } - fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { let FuncGeometry { width, height, @@ -316,7 +341,7 @@ impl Renderer for RgbState { self.values.clone() } - fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { let FuncGeometry { width, height, @@ -478,7 +503,7 @@ impl Renderer for StoreFrequencyState { self.counts.clone() } - fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { let FuncGeometry { width, height, @@ -639,7 +664,7 @@ impl Renderer for LoadFrequencyState { self.counts.clone() } - fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { let FuncGeometry { width, height, @@ -816,7 +841,7 @@ impl Renderer for RedundantState { self.redundant_store_counts.clone() } - fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { let FuncGeometry { width, height, @@ -1062,7 +1087,7 @@ impl ReuseDistanceState { self.max_reuse_distance.clone() } - pub fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + pub fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { let FuncGeometry { width, height, @@ -1306,7 +1331,7 @@ impl ThreadState { (&self.store_counts, &self.load_counts) } - pub fn to_nan_inf_overlay(&self, include_nan: bool, include_inf: bool) -> Vec { + pub fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { let FuncGeometry { width, height, diff --git a/apps/halidoscope/src/App.css b/apps/halidoscope/src/App.css index 12e2cced0969..6989d363b4d9 100644 --- a/apps/halidoscope/src/App.css +++ b/apps/halidoscope/src/App.css @@ -19,18 +19,6 @@ --zoom-level: 1; } -/* Chrome, Safari, Edge, Opera */ -input::-webkit-outer-spin-button, -input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* Firefox */ -input[type="number"] { - -moz-appearance: textfield; -} - @theme { --color-ps-primary: oklch(0.4423 0 0); --color-ps-secondary: oklch(0.2768 0 0); @@ -58,6 +46,42 @@ input[type="number"] { } } +@layer base { + /* Color Picker */ + ::-webkit-color-swatch-wrapper { + @apply p-0; + } + + ::-webkit-color-swatch { + @apply rounded-sm border-0; + } + + ::-moz-color-swatch { + @apply border-0; + } + + ::-moz-focus-inner { + @apply border-0; + } + + ::-moz-focus-inner { + @apply p-0; + } + + /* Number Input */ + /* Chrome, Safari, Edge, Opera */ + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + /* Firefox */ + input[type="number"] { + -moz-appearance: textfield; + } +} + @layer components { @keyframes slideDown { from { diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 21f8d97234e8..637a9ee866e1 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -125,8 +125,14 @@ function FuncNode({ data }: NodeProps>) { width, height, includeTabularData: active, - includeNan: nan.active, - includeInf: inf.active, + includeNan: { + active: nan.active, + ...nan.color, + }, + includeInf: { + active: inf.active, + ...inf.color, + }, }; switch (render.renderMode) { @@ -215,8 +221,8 @@ function FuncNode({ data }: NodeProps>) { activeFunc, setTabularData, thread, - nan.active, - inf.active, + nan, + inf, ]); return ( diff --git a/apps/halidoscope/src/components/controls/ControlTabs.tsx b/apps/halidoscope/src/components/controls/ControlTabs.tsx index 209255a5e78c..932188370f39 100644 --- a/apps/halidoscope/src/components/controls/ControlTabs.tsx +++ b/apps/halidoscope/src/components/controls/ControlTabs.tsx @@ -1,49 +1,68 @@ import { Tabs } from "radix-ui"; import DebugPanel from "@/components/controls/DebugPanel"; +import DisplayPanel from "@/components/controls/DisplayPanel"; import FuncsPanel from "@/components/controls/FuncsPanel"; import VisualizationPanel from "@/components/controls/VisualizationPanel"; import { FuncMeta } from "@/types"; function ControlTabs({ funcs }: { funcs: Record }) { return ( -
+
- - - - Funcs - - - Visualization - - - Debug - - - - - - - - - - - - +
+ + + + Funcs + + + Visualization + + + Debug + + + + + + + + + + + + + + + + Display + + + + + + +
); } diff --git a/apps/halidoscope/src/components/controls/DebugPanel.tsx b/apps/halidoscope/src/components/controls/DebugPanel.tsx index 5f5ac210decd..750f454e9df1 100644 --- a/apps/halidoscope/src/components/controls/DebugPanel.tsx +++ b/apps/halidoscope/src/components/controls/DebugPanel.tsx @@ -2,11 +2,16 @@ import { Separator } from "radix-ui"; import ControlSection from "@/components/controls/ControlSection"; import InfControls from "@/components/controls/inf/InfControls"; +import LivenessControls from "@/components/controls/liveness/LivenessControls"; import NaNControls from "@/components/controls/nan/NaNControls"; function DebugPanel() { return (
+ + + + @@ -14,6 +19,7 @@ function DebugPanel() { +
); } diff --git a/apps/halidoscope/src/components/controls/DisplayPanel.tsx b/apps/halidoscope/src/components/controls/DisplayPanel.tsx new file mode 100644 index 000000000000..4e332a104a50 --- /dev/null +++ b/apps/halidoscope/src/components/controls/DisplayPanel.tsx @@ -0,0 +1,21 @@ +import { Separator } from "radix-ui"; + +import ControlSection from "@/components/controls/ControlSection"; +import GraphDisplay from "@/components/controls/graph/GraphDisplay"; +import PlaybackRate from "@/components/controls/playback/PlaybackRate"; + +function DisplayPanel() { + return ( +
+ + + + + + + +
+ ); +} + +export default DisplayPanel; diff --git a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx index 6d188c643001..6815d08eafce 100644 --- a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -6,11 +6,8 @@ import * as React from "react"; import ControlSection from "@/components/controls/ControlSection"; import BarChart from "@/components/controls/bar-chart/BarChart"; import BarChartParameters from "@/components/controls/bar-chart/BarChartParameters"; -import GraphDisplay from "@/components/controls/graph/GraphDisplay"; import Histogram from "@/components/controls/histogram/Histogram"; import HistogramParameters from "@/components/controls/histogram/HistogramParameters"; -import LivenessControls from "@/components/controls/liveness/LivenessControls"; -import PlaybackRate from "@/components/controls/playback/PlaybackRate"; import RenderMode from "@/components/controls/render/RenderMode"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; @@ -201,17 +198,6 @@ function VisualizationPanel() { {renderChart()} - - - - - - - - - - -
); } diff --git a/apps/halidoscope/src/components/controls/color/ColorInput.tsx b/apps/halidoscope/src/components/controls/color/ColorInput.tsx new file mode 100644 index 000000000000..b8b12874af7a --- /dev/null +++ b/apps/halidoscope/src/components/controls/color/ColorInput.tsx @@ -0,0 +1,118 @@ +import * as d3 from "d3"; +import { Label } from "radix-ui"; +import * as React from "react"; + +interface Props { + id: string; + color: string; + defaultValue: string; + onChangeColor: (color: string) => void; + alpha: number; + onChangeAlpha: (opacity: number) => void; +} + +const hexPattern = /^#([0-9A-Fa-f]{3}){1,2}$/i; + +function ColorInput({ + id, + color, + defaultValue, + onChangeColor, + alpha, + onChangeAlpha, +}: Props) { + const [colorLocal, setColorLocal] = React.useState(color); + const [alphaLocal, setAlphaLocal] = React.useState(alpha); + + const onChange = React.useCallback( + (event: React.ChangeEvent) => { + let output = event.currentTarget.value; + setColorLocal(output); + + if (!output.startsWith("#")) { + output = "#" + output; + } + + if (hexPattern.test(output)) { + onChangeColor(d3.color(output)?.formatHex() ?? defaultValue); + } + }, + [defaultValue, onChangeColor], + ); + + const onBlur = React.useCallback(() => { + let c = color; + + if (!color.startsWith("#")) { + c = "#" + c; + } + + if (!hexPattern.test(c)) { + setColorLocal(defaultValue); + onChangeColor(defaultValue); + } + }, [color, onChangeColor, defaultValue]); + + const onChangeAlphaLocal = React.useCallback( + (event: React.ChangeEvent) => { + const output = Number(event.currentTarget.value); + setAlphaLocal(output); + + if (output >= 0 && output <= 100) { + onChangeAlpha(output); + } + }, + [setAlphaLocal, onChangeAlpha], + ); + + const onBlurAlphaLocal = React.useCallback(() => { + const a = alphaLocal; + + if (Number.isNaN(a)) { + setAlphaLocal(alpha); + } else if (a < 0) { + setAlphaLocal(0); + onChangeAlpha(0); + } else if (a > 100) { + setAlphaLocal(100); + onChangeAlpha(100); + } + }, [alpha, alphaLocal, onChangeAlpha]); + + return ( +
+ + Color + +
+ + + + % +
+
+ ); +} + +export default ColorInput; diff --git a/apps/halidoscope/src/components/controls/inf/InfControls.tsx b/apps/halidoscope/src/components/controls/inf/InfControls.tsx index ea0587cfd16a..8650ba89063b 100644 --- a/apps/halidoscope/src/components/controls/inf/InfControls.tsx +++ b/apps/halidoscope/src/components/controls/inf/InfControls.tsx @@ -1,9 +1,11 @@ +import * as d3 from "d3"; import { useAtom } from "jotai"; import { Checkbox, Label, Select } from "radix-ui"; +import ColorInput from "@/components/controls/color/ColorInput"; import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; import CheckIcon from "@/components/icons/CheckIcon"; -import { infAtom } from "@/state/inf"; +import { DEFAULT_INF_COLOR, infAtom } from "@/state/inf"; import type { AnimationMode } from "@/types"; import { ANIMATION_MODES } from "@/utils/constants"; @@ -27,46 +29,68 @@ function InfControls() {
{inf.active ? ( -
- - Animation Mode - - - setInf({ ...inf, animationMode: value as AnimationMode }) - } - > - +
+ - - - - - - + + setInf({ ...inf, animationMode: value as AnimationMode }) + } > - - {ANIMATION_MODES.map((value) => ( - - {value} - - ))} - - - + + + + + + + + + {ANIMATION_MODES.map((value) => ( + + {value} + + ))} + + + +
+ { + const { r, g, b } = d3.color(color)?.rgb() ?? { + r: 0, + g: 0, + b: 0, + }; + setInf({ ...inf, color: { ...inf.color, r, g, b } }); + }} + alpha={Math.round(inf.color.a * 100)} + onChangeAlpha={(alpha) => { + setInf({ + ...inf, + color: { ...inf.color, a: alpha / 100 }, + }); + }} + />
) : null}
diff --git a/apps/halidoscope/src/components/controls/nan/NaNControls.tsx b/apps/halidoscope/src/components/controls/nan/NaNControls.tsx index 04d4789c901d..17a40c7e5c1c 100644 --- a/apps/halidoscope/src/components/controls/nan/NaNControls.tsx +++ b/apps/halidoscope/src/components/controls/nan/NaNControls.tsx @@ -1,9 +1,11 @@ +import * as d3 from "d3"; import { useAtom } from "jotai"; import { Checkbox, Label, Select } from "radix-ui"; +import ColorInput from "@/components/controls/color/ColorInput"; import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; import CheckIcon from "@/components/icons/CheckIcon"; -import { nanAtom } from "@/state/nan"; +import { DEFAULT_NAN_COLOR, nanAtom } from "@/state/nan"; import type { AnimationMode } from "@/types"; import { ANIMATION_MODES } from "@/utils/constants"; @@ -27,46 +29,68 @@ function NaNControls() {
{nan.active ? ( -
- - Animation Mode - - - setNan({ ...nan, animationMode: value as AnimationMode }) - } - > - +
+ - - - - - - + + setNan({ ...nan, animationMode: value as AnimationMode }) + } > - - {ANIMATION_MODES.map((value) => ( - - {value} - - ))} - - - + + + + + + + + + {ANIMATION_MODES.map((value) => ( + + {value} + + ))} + + + +
+ { + const { r, g, b } = d3.color(color)?.rgb() ?? { + r: 0, + g: 0, + b: 0, + }; + setNan({ ...nan, color: { ...nan.color, r, g, b } }); + }} + alpha={Math.round(nan.color.a * 100)} + onChangeAlpha={(alpha) => { + setNan({ + ...nan, + color: { ...nan.color, a: alpha / 100 }, + }); + }} + />
) : null}
diff --git a/apps/halidoscope/src/state/inf.ts b/apps/halidoscope/src/state/inf.ts index 66f6ca75bdf2..a0cce1602a15 100644 --- a/apps/halidoscope/src/state/inf.ts +++ b/apps/halidoscope/src/state/inf.ts @@ -2,7 +2,25 @@ import { atom } from "jotai"; import { AnimationMode } from "@/types"; -export const infAtom = atom<{ active: boolean; animationMode: AnimationMode }>({ +export const DEFAULT_INF_COLOR = "#ffff00"; +export const DEFAULT_INF_ALPHA = 1; + +export const infAtom = atom<{ + active: boolean; + animationMode: AnimationMode; + color: { + r: number; + g: number; + b: number; + a: number; + }; +}>({ active: false, animationMode: "Blink", + color: { + r: 255, + g: 255, + b: 0, + a: DEFAULT_INF_ALPHA, + }, }); diff --git a/apps/halidoscope/src/state/nan.ts b/apps/halidoscope/src/state/nan.ts index e21caa1bdc57..b6b62696119f 100644 --- a/apps/halidoscope/src/state/nan.ts +++ b/apps/halidoscope/src/state/nan.ts @@ -2,7 +2,25 @@ import { atom } from "jotai"; import { AnimationMode } from "@/types"; -export const nanAtom = atom<{ active: boolean; animationMode: AnimationMode }>({ +export const DEFAULT_NAN_COLOR = "#00ffff"; +export const DEFAULT_NAN_ALPHA = 1; + +export const nanAtom = atom<{ + active: boolean; + animationMode: AnimationMode; + color: { + r: number; + g: number; + b: number; + a: number; + }; +}>({ active: false, animationMode: "Blink", + color: { + r: 0, + g: 255, + b: 255, + a: DEFAULT_NAN_ALPHA, + }, }); diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index 12a861daff30..f89ee23f733c 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -22,8 +22,20 @@ export interface RenderFuncParams { width: number; height: number; includeTabularData: boolean; - includeNan: boolean; - includeInf: boolean; + includeNan: { + active: boolean; + r: number; + g: number; + b: number; + a: number; + }; + includeInf: { + active: boolean; + r: number; + g: number; + b: number; + a: number; + }; } /** From 8ee9b76683473ca306d5174b8d61b490f0a47f64 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Mon, 10 Aug 2026 10:55:36 -0700 Subject: [PATCH 34/67] Ignore demos. --- apps/halidoscope/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/halidoscope/.gitignore b/apps/halidoscope/.gitignore index 0107ad7ddde4..952d7ae1d579 100644 --- a/apps/halidoscope/.gitignore +++ b/apps/halidoscope/.gitignore @@ -26,3 +26,4 @@ dist-ssr # Trace binaries *.hltrace /samples/ +/demos/ From 7982b075b662d9c94f58ba5ce1065dcc1e5db567 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Wed, 29 Jul 2026 22:23:27 -0700 Subject: [PATCH 35/67] Add support for Grayscale render mode histograms. --- apps/halidoscope/src-tauri/Cargo.lock | 131 ++++++++++-- apps/halidoscope/src-tauri/Cargo.toml | 3 +- apps/halidoscope/src-tauri/src/cli.rs | 2 +- apps/halidoscope/src-tauri/src/colormap.rs | 73 +++++++ apps/halidoscope/src-tauri/src/commands.rs | 14 +- apps/halidoscope/src-tauri/src/lib.rs | 1 + apps/halidoscope/src-tauri/src/render.rs | 181 +++++++++++------ .../controls/VisualizationPanel.tsx | 105 +++++++--- .../controls/bar-chart/BarChart.tsx | 12 +- .../controls/histogram/Histogram.tsx | 34 +--- .../histogram/HistogramParameters.tsx | 186 +++++++++--------- apps/halidoscope/src/utils/api.ts | 4 +- 12 files changed, 522 insertions(+), 224 deletions(-) create mode 100644 apps/halidoscope/src-tauri/src/colormap.rs diff --git a/apps/halidoscope/src-tauri/Cargo.lock b/apps/halidoscope/src-tauri/Cargo.lock index 70fa7ccff162..72d73ba29597 100644 --- a/apps/halidoscope/src-tauri/Cargo.lock +++ b/apps/halidoscope/src-tauri/Cargo.lock @@ -97,6 +97,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -372,6 +381,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytemuck" version = "1.25.0" @@ -709,7 +724,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf", + "phf 0.13.1", "smallvec", ] @@ -1062,6 +1077,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastrand" version = "2.4.1" @@ -1540,6 +1561,7 @@ version = "0.1.0" dependencies = [ "colorous", "comfy-table", + "palette", "serde", "serde_json", "tauri", @@ -2488,6 +2510,30 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf 0.11.3", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pango" version = "0.18.3" @@ -2554,14 +2600,24 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros", - "phf_shared", + "phf_macros 0.13.1", + "phf_shared 0.13.1", "serde", ] @@ -2571,8 +2627,18 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand", ] [[package]] @@ -2582,7 +2648,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2591,13 +2670,22 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "phf_shared" version = "0.13.1" @@ -2806,6 +2894,21 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3026,7 +3129,7 @@ dependencies = [ "derive_more", "log", "new_debug_unreachable", - "phf", + "phf 0.13.1", "phf_codegen", "precomputed-hash", "rustc-hash", @@ -3325,7 +3428,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared", + "phf_shared 0.13.1", "precomputed-hash", ] @@ -3335,8 +3438,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", ] @@ -3704,7 +3807,7 @@ dependencies = [ "json-patch", "log", "memchr", - "phf", + "phf 0.13.1", "plist", "proc-macro2", "quote", @@ -4441,7 +4544,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ - "phf", + "phf 0.13.1", "phf_codegen", "string_cache", "string_cache_codegen", diff --git a/apps/halidoscope/src-tauri/Cargo.toml b/apps/halidoscope/src-tauri/Cargo.toml index 57572c6d0563..8b14e7b4618a 100644 --- a/apps/halidoscope/src-tauri/Cargo.toml +++ b/apps/halidoscope/src-tauri/Cargo.toml @@ -22,8 +22,9 @@ tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -colorous = "1.0.16" comfy-table = "7" +palette = "0.7.6" +colorous = "1.0.16" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-cli = "2.0.0" diff --git a/apps/halidoscope/src-tauri/src/cli.rs b/apps/halidoscope/src-tauri/src/cli.rs index ab6badc712ff..564e2c1cdc54 100644 --- a/apps/halidoscope/src-tauri/src/cli.rs +++ b/apps/halidoscope/src-tauri/src/cli.rs @@ -314,7 +314,7 @@ fn snapshot(subcommand: Box) -> Option<()> { std::process::exit(1); }; - rs.seek(&trace, store_indices, k); + rs.seek(&trace, store_indices, load_indices, k, k); serde_json::to_string_pretty(&rs.to_values()).unwrap_or_else(|e| { eprintln!("Error serializing values to JSON: {}", e); std::process::exit(1); diff --git a/apps/halidoscope/src-tauri/src/colormap.rs b/apps/halidoscope/src-tauri/src/colormap.rs new file mode 100644 index 000000000000..8792464b8e43 --- /dev/null +++ b/apps/halidoscope/src-tauri/src/colormap.rs @@ -0,0 +1,73 @@ +use palette::{Mix, Srgb}; + +/// The palette used for displaying core metrics in Halidoscope, including Store Frequency, Load +/// Frequency, Redundant Stores, and Reuse Distance. +pub const METRIC_PALETTE: [&str; 10] = [ + "#0078D1", "#1695F3", "#3DACFF", "#70C2FF", "#D6EEFF", "#FFE2D6", "#FFBFA3", "#FF773D", + "#FA6400", "#D64000", +]; + +pub struct Colormap { + stops: Vec>, +} + +impl Colormap { + pub fn from_hex(hex_colors: &[&str]) -> Self { + Self { + stops: hex_colors.iter().map(|hex| parse_hex(hex)).collect(), + } + } + + /// Samples the gradient at `i / 255` for `i` in `[0, 255]` to build an LUT. + pub fn to_lut(&self) -> [[u8; 3]; 256] { + std::array::from_fn(|i| self.eval(i as f64 / 255.0)) + } + + fn eval(&self, t: f64) -> [u8; 3] { + let segments = self.stops.len() - 1; + let t = (t.clamp(0.0, 1.0) as f32) * segments as f32; + let i = (t.floor() as usize).min(segments - 1); + let local_t = t - i as f32; + + let color: Srgb = self.stops[i].mix(self.stops[i + 1], local_t).into_format(); + [color.red, color.green, color.blue] + } +} + +fn parse_hex(hex: &str) -> Srgb { + let hex = hex.trim_start_matches('#'); + let r = u8::from_str_radix(&hex[0..2], 16).expect("valid hex color"); + let g = u8::from_str_radix(&hex[2..4], 16).expect("valid hex color"); + let b = u8::from_str_radix(&hex[4..6], 16).expect("valid hex color"); + + Srgb::new(r, g, b).into_format() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matches_d3_piecewise_reference() { + let cmap = Colormap::from_hex(&METRIC_PALETTE); + let cases: [(f64, &str); 11] = [ + (0.0, "#0078d1"), + (0.01, "#027bd4"), + (0.12, "#1997f4"), + (0.33, "#6ec1ff"), + (0.495, "#e9e9ec"), + (0.50, "#ebe8eb"), + (0.505, "#ece7e9"), + (0.66, "#ffc1a6"), + (0.87, "#fb670a"), + (0.99, "#d94300"), + (1.0, "#d64000"), + ]; + + for (t, expected) in cases { + let [r, g, b] = cmap.eval(t); + let got = format!("#{r:02x}{g:02x}{b:02x}"); + assert_eq!(got, expected, "t={t}"); + } + } +} diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 0024d14b1338..49dca4fcfaf6 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -241,6 +241,7 @@ pub fn render_grayscale( func: String, global_index: u32, normalization_mode: NormalizationMode, + include_tabular_data: bool, include_nan: IncludeNan, include_inf: IncludeInf, state: State, @@ -265,11 +266,16 @@ pub fn render_grayscale( renderer.seek(trace, store_indices, k); let pixels = renderer.to_rgba(normalization_mode); + let histogram = if include_tabular_data { + renderer.to_histogram() + } else { + Vec::new() + }; let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); Ok(Response::new(pack_render_response( pixels, nan_inf_overlays, - &[], + &histogram, ))) } @@ -434,8 +440,10 @@ pub fn render_redundant_stores( let renderer = redundant_renderers.get_mut(&func).expect("just inserted"); let store_indices = trace.func_store_indices(&func).unwrap_or(&[]); - let k = store_indices.partition_point(|&p| p <= global_index as usize); - renderer.seek(trace, store_indices, k); + let load_indices = trace.func_load_indices(&func).unwrap_or(&[]); + let store_k = store_indices.partition_point(|&p| p <= global_index as usize); + let load_k = load_indices.partition_point(|&p| p <= global_index as usize); + renderer.seek(trace, store_indices, load_indices, store_k, load_k); let pixels = renderer.to_rgba(normalization_mode); let histogram = if include_tabular_data { diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index c05c0de0dc14..07662cb93b57 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ use tauri_plugin_cli::CliExt; use crate::cli::halidoscope_cli; pub mod cli; +pub mod colormap; pub mod commands; pub mod graph; pub mod render; diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index fa2bf1f22c3f..604ab762f047 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -3,6 +3,7 @@ use std::vec; use ::colorous; use serde::Deserialize; +use crate::colormap::{Colormap, METRIC_PALETTE}; use crate::trace::{for_each_lane_pixel, FuncGeometry, Trace, TracePacket}; #[derive(Deserialize, Clone, Copy)] @@ -154,6 +155,33 @@ impl GrayscaleState { fn normalize(&self, v: f64) -> u8 { (255.0 * (v - self.min_v) / (self.max_v - self.min_v)).clamp(0.0, 255.0) as u8 } + + /// Bins the raw (pre-normalization) intensity displayed per pixel — the same luma blend + /// `to_rgba` uses for channels >= 3, or the raw channel-0 value otherwise — into 256 + /// fixed-width buckets (one per displayable 8-bit gray level) spanning `[min_v, max_v]`. + pub fn to_histogram(&self) -> Vec { + const NUM_BINS: usize = 256; + let channels = self.geom.channels; + let range = self.max_v - self.min_v; + + let mut bins = vec![0u32; NUM_BINS]; + for src in self.values.chunks_exact(channels) { + let v = if channels >= 3 { + src[0] * 0.2125 + src[1] * 0.7154 + src[2] * 0.0721 + } else { + src[0] + }; + + let bucket = if range > 0.0 { + (((v - self.min_v) / range) * NUM_BINS as f64) as usize + } else { + 0 + }; + bins[bucket.min(NUM_BINS - 1)] += 1; + } + + bins + } } impl Renderer for GrayscaleState { @@ -468,11 +496,7 @@ impl Renderer for StoreFrequencyState { fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; - let gradient = colorous::INFERNO; - let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { - let c = gradient.eval_continuous(i as f64 / 255.0); - [c.r, c.g, c.b] - }); + let lut = Colormap::from_hex(&METRIC_PALETTE).to_lut(); let scale = match (normalization_mode, self.global_max_store_count) { (NormalizationMode::AcrossFuncs, 0) => 0.0, @@ -630,11 +654,7 @@ impl Renderer for LoadFrequencyState { fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; - let gradient = colorous::INFERNO; - let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { - let c = gradient.eval_continuous(i as f64 / 255.0); - [c.r, c.g, c.b] - }); + let lut = Colormap::from_hex(&METRIC_PALETTE).to_lut(); let scale = match (normalization_mode, self.global_max_load_count) { (NormalizationMode::AcrossFuncs, 0) => 0.0, @@ -691,7 +711,8 @@ pub struct RedundantState { redundant_store_counts: Vec, local_max_redundant_store_count: u32, global_max_redundant_store_count: u32, - applied_k: usize, + applied_store_k: usize, + applied_load_k: usize, } impl RedundantState { @@ -714,14 +735,16 @@ impl RedundantState { redundant_store_counts: vec![0u32; n_pixels], local_max_redundant_store_count, global_max_redundant_store_count, - applied_k: 0, + applied_store_k: 0, + applied_load_k: 0, }) } fn reset(&mut self) { self.last_values.iter_mut().for_each(|v| *v = None); self.redundant_store_counts.iter_mut().for_each(|c| *c = 0); - self.applied_k = 0; + self.applied_store_k = 0; + self.applied_load_k = 0; } fn apply_store(&mut self, pkt: &TracePacket) { @@ -758,55 +781,77 @@ impl RedundantState { ); } - pub fn to_tabular_data(&self, normalization_mode: NormalizationMode) -> Vec { - let max = match normalization_mode { - NormalizationMode::AcrossFuncs => self.global_max_redundant_store_count, - NormalizationMode::PerFunc => self.local_max_redundant_store_count, - }; + /// A load observes the current value at `(x, y, channel)`, so it breaks the redundancy chain: + /// clear the last-written value there so a subsequent store is never counted as redundant + /// against a value that predates the load. + fn apply_load(&mut self, pkt: &TracePacket) { + let FuncGeometry { + width, + height, + channels, + min_x, + min_y, + min_c, + .. + } = self.geom; - let exceeds_max_bins = max > 64; - // Pre-allocate tabular_data, capping to 64 bins. - let mut tabular_data = vec![ - 0u32; - if exceeds_max_bins { - 64 - } else { - max as usize + 1 - } - ]; + for_each_lane_pixel( + pkt, + min_x, + min_y, + width, + height, + Some((min_c, channels)), + |_lane, _pixel_idx, val_idx: usize| { + self.last_values[val_idx] = None; + }, + ); + } - for &c in &self.redundant_store_counts { - let bucket = if exceeds_max_bins { c * 63 / max } else { c }; + /// Seeks to the state after the first `target_store_k` stores and `target_load_k` loads. + /// Events are replayed in global packet order via a two-pointer merge of the two sorted index + /// lists. Backward seeks (either counter regresses) reset and replay from zero. + pub fn seek( + &mut self, + trace: &Trace, + store_indices: &[usize], + load_indices: &[usize], + target_store_k: usize, + target_load_k: usize, + ) { + let target_store_k = target_store_k.min(store_indices.len()); + let target_load_k = target_load_k.min(load_indices.len()); - tabular_data[bucket.clamp(0, 63) as usize] += 1; + if target_store_k < self.applied_store_k || target_load_k < self.applied_load_k { + self.reset(); } - tabular_data - } -} + let store_slice = &store_indices[self.applied_store_k..target_store_k]; + let load_slice = &load_indices[self.applied_load_k..target_load_k]; + let mut si = 0; + let mut li = 0; -impl Renderer for RedundantState { - type Value = u32; + while si < store_slice.len() || li < load_slice.len() { + let next_is_store = si < store_slice.len() + && (li >= load_slice.len() || store_slice[si] < load_slice[li]); - fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize) { - let target_k = target_k.min(store_indices.len()); - if target_k < self.applied_k { - self.reset(); - } - for &global_idx in &store_indices[self.applied_k..target_k] { - self.apply_store(&trace.packets[global_idx]); + if next_is_store { + self.apply_store(&trace.packets[store_slice[si]]); + si += 1; + } else { + self.apply_load(&trace.packets[load_slice[li]]); + li += 1; + } } - self.applied_k = target_k; + + self.applied_store_k = target_store_k; + self.applied_load_k = target_load_k; } - fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { + pub fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; - let gradient = colorous::INFERNO; - let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { - let c = gradient.eval_continuous(i as f64 / 255.0); - [c.r, c.g, c.b] - }); + let lut = Colormap::from_hex(&METRIC_PALETTE).to_lut(); let scale = match (normalization_mode, self.global_max_redundant_store_count) { (NormalizationMode::AcrossFuncs, 0) => 0.0, @@ -837,11 +882,37 @@ impl Renderer for RedundantState { out } - fn to_values(&self) -> Vec { + pub fn to_tabular_data(&self, normalization_mode: NormalizationMode) -> Vec { + let max = match normalization_mode { + NormalizationMode::AcrossFuncs => self.global_max_redundant_store_count, + NormalizationMode::PerFunc => self.local_max_redundant_store_count, + }; + + let exceeds_max_bins = max > 64; + // Pre-allocate tabular_data, capping to 64 bins. + let mut tabular_data = vec![ + 0u32; + if exceeds_max_bins { + 64 + } else { + max as usize + 1 + } + ]; + + for &c in &self.redundant_store_counts { + let bucket = if exceeds_max_bins { c * 63 / max } else { c }; + + tabular_data[bucket.clamp(0, 63) as usize] += 1; + } + + tabular_data + } + + pub fn to_values(&self) -> Vec { self.redundant_store_counts.clone() } - fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { + pub fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { let FuncGeometry { width, height, @@ -1032,11 +1103,7 @@ impl ReuseDistanceState { pub fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec { let FuncGeometry { width, height, .. } = self.geom; - let gradient = colorous::INFERNO; - let lut: [[u8; 3]; 256] = std::array::from_fn(|i| { - let c = gradient.eval_continuous(i as f64 / 255.0); - [c.r, c.g, c.b] - }); + let lut = Colormap::from_hex(&METRIC_PALETTE).to_lut(); let scale = match (normalization_mode, self.global_max_reuse_distance) { (NormalizationMode::AcrossFuncs, 0) => 0.0, diff --git a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx index 6815d08eafce..ab9fe9d4215a 100644 --- a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -16,7 +16,7 @@ import { tabularDataAtom } from "@/state/tabularData"; import { threadAtom, NO_THREAD_INFO_SENTINEL_ID } from "@/state/thread"; const RENDER_MODE_TO_LABEL: Record = { - Grayscale: "", + Grayscale: "Value", RGB: "", "Store Frequency": "Store Count", "Load Frequency": "Load Count", @@ -25,25 +25,38 @@ const RENDER_MODE_TO_LABEL: Record = { "Thread Coverage": "Thread ID", }; +const METRIC_PALETTE = [ + "#0078D1", + "#1695F3", + "#3DACFF", + "#70C2FF", + "#D6EEFF", + "#FFE2D6", + "#FFBFA3", + "#FF773D", + "#FA6400", + "#D64000", +]; + interface HistogramData { type: "Histogram"; data: { x1: number; x2: number; y: number }[]; domain: [number, number]; - lut: Record; + range: string[]; } interface BarChartData { type: "Bar Chart"; data: { x: string; y: number }[]; domain: string[]; - lut: Record; + range: string[]; } interface NoChartData { type: "No Chart"; data: number[]; domain: [number, number]; - lut: Record; + range: string[]; } type ChartData = HistogramData | BarChartData | NoChartData; @@ -54,59 +67,95 @@ function VisualizationPanel() { const activeFunc = useAtomValue(funcAtom); const { tabularData, scale } = useAtomValue(tabularDataAtom); const thread = useAtomValue(threadAtom); - const min = scale === "log" ? 1 : 0; const createHistogramData = React.useCallback( - (histogramData: Uint32Array, max: number): HistogramData => { + ( + histogramData: Uint32Array, + domain: [number, number], + range: string[], + ): HistogramData => { const buckets = histogramData.length; + const extent = domain[1] - domain[0]; + const step = + domain.every(Number.isInteger) && extent <= 64 ? 1 : extent / buckets; return { type: "Histogram", data: new Array(buckets).fill(0).map((_, i) => ({ - x1: max > 64 ? Math.round((i / 64) * max) : i, - x2: max > 64 ? Math.round(((i + 1) / 64) * max) : i + 1, + x1: domain[0] + i * step, + x2: domain[0] + (i + 1) * step, y: histogramData?.[i] ?? 0, })), - domain: [min, max + 1], - lut: {}, + domain, + range, }; }, - [min], + [], ); - const { type, data, domain, lut } = React.useMemo((): ChartData => { + const { type, data, domain, range } = React.useMemo((): ChartData => { switch (render.renderMode) { + case "Grayscale": { + const min = funcs[activeFunc].min_value ?? 0; + const max = funcs[activeFunc].max_value ?? 255; + + return createHistogramData( + tabularData ?? new Uint32Array(), + [min, max], + ["#000000", "#ffffff"], + ); + } case "Store Frequency": { + const min = scale === "log" ? 1 : 0; const max = render.normalizationMode === "Per Func" ? funcs[activeFunc].max_store_count : stats.global_max_store_count; - return createHistogramData(tabularData ?? new Uint32Array(), max); + return createHistogramData( + tabularData ?? new Uint32Array(), + [min, max], + METRIC_PALETTE, + ); } case "Load Frequency": { + const min = scale === "log" ? 1 : 0; const max = render.normalizationMode === "Per Func" ? funcs[activeFunc].max_load_count : stats.global_max_load_count; - return createHistogramData(tabularData ?? new Uint32Array(), max); + return createHistogramData( + tabularData ?? new Uint32Array(), + [min, max], + METRIC_PALETTE, + ); } case "Redundant Stores": { + const min = scale === "log" ? 1 : 0; const max = render.normalizationMode === "Per Func" ? funcs[activeFunc].max_redundant_store_count : stats.global_max_redundant_store_count; - return createHistogramData(tabularData ?? new Uint32Array(), max); + return createHistogramData( + tabularData ?? new Uint32Array(), + [min, max], + METRIC_PALETTE, + ); } case "Reuse Distance": { + const min = scale === "log" ? 1 : 0; const max = render.normalizationMode === "Per Func" ? funcs[activeFunc].max_reuse_distance : stats.global_max_reuse_distance; - return createHistogramData(tabularData ?? new Uint32Array(), max); + return createHistogramData( + tabularData ?? new Uint32Array(), + [min, max], + METRIC_PALETTE, + ); } case "Thread Coverage": { const threadIds = funcs[activeFunc].thread_ids; @@ -121,18 +170,17 @@ function VisualizationPanel() { y: thread.op === "Store" ? storeCounts[i] : loadCounts[i], })), domain: threadIds.map((tId) => `${tId}`), - lut: stats.global_thread_ids.reduce>( - (acc, el, i) => { - acc[el] = d3.schemeSet3[i]; - - return acc; - }, - {}, - ), + range: stats.global_thread_ids.reduce((acc, el, i) => { + if (threadIds.includes(el)) { + return acc.concat(d3.schemeSet3[i]); + } + + return acc; + }, []), }; } default: { - return { type: "No Chart", data: [], domain: [-1, -1], lut: {} }; + return { type: "No Chart", data: [], domain: [-1, -1], range: [] }; } } }, [ @@ -141,6 +189,7 @@ function VisualizationPanel() { funcs, stats, activeFunc, + scale, thread.op, createHistogramData, ]); @@ -156,6 +205,8 @@ function VisualizationPanel() { x === thread.id || thread.id === NO_THREAD_INFO_SENTINEL_ID } @@ -190,7 +241,7 @@ function VisualizationPanel() { case "No Chart": return ; } - }, [type, data, domain, lut, render.renderMode, thread]); + }, [type, data, domain, range, scale, render.renderMode, thread]); return (
diff --git a/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx b/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx index bced6ec49700..dde9ddd85763 100644 --- a/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx +++ b/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx @@ -5,15 +5,15 @@ import * as React from "react"; interface BarChartProps { data: { x: string; y: number }[]; domain: string[]; + range: string[]; labels: { x: string; y: string; }; - lut: Record; highlight?: (x: string) => boolean; } -function BarChart({ data, domain, labels, lut, highlight }: BarChartProps) { +function BarChart({ data, domain, range, labels, highlight }: BarChartProps) { const ref = React.useRef(null); React.useEffect(() => { @@ -42,11 +42,15 @@ function BarChart({ data, domain, labels, lut, highlight }: BarChartProps) { tickRotate: -45, type: "band", }, + color: { + domain, + range, + }, marks: [ Plot.barY(data, { x: "x", y: "y", - fill: (d) => lut[d.x] ?? "#000000", + fill: "x", fillOpacity: (d) => (highlight?.(d.x) ? 1 : 0.25), }), ], @@ -57,7 +61,7 @@ function BarChart({ data, domain, labels, lut, highlight }: BarChartProps) { return () => { plot.remove(); }; - }, [data, labels, domain, lut, highlight]); + }, [data, labels, domain, range, highlight]); return data.every((d) => d.y === 0) ? (
diff --git a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx index 1e74bf2f7483..cf463f8501b1 100644 --- a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx +++ b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx @@ -1,34 +1,22 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; -import { useAtomValue } from "jotai"; import * as React from "react"; -import { tabularDataAtom } from "@/state/tabularData"; +import type { Scale } from "@/state/tabularData"; interface HistogramProps { data: { x1: number; x2: number; y: number }[]; domain: [number, number]; + range: string[]; + scale: Scale; labels: { x: string; y: string; }; } -function Histogram({ data, domain, labels }: HistogramProps) { +function Histogram({ data, domain, scale, range, labels }: HistogramProps) { const ref = React.useRef(null); - const { scale } = useAtomValue(tabularDataAtom); - - // Build the data for the bottom colorbar. - const colorbar = React.useMemo(() => { - const range = domain[1] - domain[0]; - const count = range <= 64 ? range : 64; - const step = range / count; - return new Array(count).fill(0).map((_, i) => ({ - x1: domain[0] + i * step, - x2: domain[0] + (i + 1) * step, - y: 0, - })); - }, [domain]); React.useEffect(() => { if (!ref.current) { @@ -48,7 +36,7 @@ function Histogram({ data, domain, labels }: HistogramProps) { ticks: 8, }, x: { - domain, + domain: [data[0].x1, data[data.length - 1].x2], label: labels.x, labelAnchor: "right", labelArrow: "right", @@ -56,18 +44,16 @@ function Histogram({ data, domain, labels }: HistogramProps) { tickPadding: 24, tickSize: 0, type: scale, - interval: domain[1] <= 64 ? 1 : undefined, }, color: { - // Constrain the color scale to the bounds used for computing the canvas - // on the backend. - domain: [0, domain[1] - 1], - scheme: "Inferno", + domain, + range, type: "linear", + interpolate: "rgb" as const, }, marks: [ Plot.rectY(data, { x1: "x1", x2: "x2", y: "y", fill: "x1" }), - Plot.ruleY(colorbar, { + Plot.ruleY(data, { stroke: "x1", strokeWidth: 8, x1: "x1", @@ -83,7 +69,7 @@ function Histogram({ data, domain, labels }: HistogramProps) { return () => { plot.remove(); }; - }, [data, domain, labels, scale, colorbar]); + }, [data, domain, labels, range, scale]); return data.every((d) => d.y === 0) ? (
diff --git a/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx b/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx index 73f67f1d9677..ca0525892145 100644 --- a/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx +++ b/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx @@ -57,103 +57,105 @@ function HistogramParameters() {
-
-
- - Scale - - - setTabularData({ ...tabularData, scale: value as Scale }) - } - > - +
+ - - - - - - + + setTabularData({ ...tabularData, scale: value as Scale }) + } > - - - Linear - - - Log - - - - -
-
- - Normalize Display - - - setRender({ - ...render, - normalizationMode: value as NormalizationMode, - }) - } - > - + + + + + + + + + Linear + + + Log + + + + +
+
+ - - - - - - + + setRender({ + ...render, + normalizationMode: value as NormalizationMode, + }) + } > - - - Across Funcs - - - Per Func - - - - + + + + + + + + + + Across Funcs + + + Per Func + + + + +
-
+ ) : null}
); } diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index f89ee23f733c..4a367e01a826 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -93,6 +93,7 @@ export async function renderGrayscale({ normalizationMode, width, height, + includeTabularData, includeNan, includeInf, }: RenderFuncParams): Promise { @@ -100,6 +101,7 @@ export async function renderGrayscale({ func, globalIndex, normalizationMode, + includeTabularData, includeNan, includeInf, }); @@ -108,7 +110,7 @@ export async function renderGrayscale({ buffer, width, height, - includeTabularData: false, + includeTabularData, }); } From eb010cccf18f5045cfbcecf56f2ca8d3d25a2cb6 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 30 Jul 2026 14:15:10 -0700 Subject: [PATCH 36/67] Add basic support for RGB histograms. Co-authored-by: Claude Opus 5 --- apps/halidoscope/src-tauri/src/commands.rs | 8 +- apps/halidoscope/src-tauri/src/render.rs | 35 +++++ .../controls/VisualizationPanel.tsx | 140 +++++++++++++++++- .../controls/histogram/Histogram.tsx | 42 ++++-- .../histogram/HistogramParameters.tsx | 2 +- apps/halidoscope/src/utils/api.ts | 4 +- 6 files changed, 205 insertions(+), 26 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 49dca4fcfaf6..40417f855a79 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -286,6 +286,7 @@ pub fn render_rgb( func: String, global_index: u32, normalization_mode: NormalizationMode, + include_tabular_data: bool, include_nan: IncludeNan, include_inf: IncludeInf, state: State, @@ -310,11 +311,16 @@ pub fn render_rgb( renderer.seek(trace, store_indices, k); let pixels = renderer.to_rgba(normalization_mode); + let histogram = if include_tabular_data { + renderer.to_histogram() + } else { + Vec::new() + }; let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); Ok(Response::new(pack_render_response( pixels, nan_inf_overlays, - &[], + &histogram, ))) } diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index 604ab762f047..f4cd792d82a2 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -317,6 +317,41 @@ impl RgbState { fn normalize(&self, v: f64) -> u8 { (255.0 * (v - self.min_v) / (self.max_v - self.min_v)).clamp(0.0, 255.0) as u8 } + + /// Bins the raw (pre-normalization) per-channel values into 256 fixed-width buckets spanning + /// `[min_v, max_v]`. When `channels >= 3`, returns three histograms back to back (R, then G, + /// then B, 256 `u32`s each — the caller recovers the channel count as `len / 256`). Otherwise + /// falls back to a single histogram over channel 0, matching `GrayscaleState::to_histogram`. + pub fn to_histogram(&self) -> Vec { + const NUM_BINS: usize = 256; + let channels = self.geom.channels; + let range = self.max_v - self.min_v; + + let bucket_of = |v: f64| -> usize { + let bucket = if range > 0.0 { + (((v - self.min_v) / range) * NUM_BINS as f64) as usize + } else { + 0 + }; + bucket.min(NUM_BINS - 1) + }; + + if channels < 3 { + let mut bins = vec![0u32; NUM_BINS]; + for src in self.values.chunks_exact(channels) { + bins[bucket_of(src[0])] += 1; + } + return bins; + } + + let mut bins = vec![0u32; NUM_BINS * 3]; + for src in self.values.chunks_exact(channels) { + for c in 0..3 { + bins[c * NUM_BINS + bucket_of(src[c])] += 1; + } + } + bins + } } impl Renderer for RgbState { diff --git a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx index ab9fe9d4215a..39560e39a162 100644 --- a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -17,7 +17,7 @@ import { threadAtom, NO_THREAD_INFO_SENTINEL_ID } from "@/state/thread"; const RENDER_MODE_TO_LABEL: Record = { Grayscale: "Value", - RGB: "", + RGB: "Value", "Store Frequency": "Store Count", "Load Frequency": "Load Count", "Redundant Stores": "Redundant Store Count", @@ -40,11 +40,29 @@ const METRIC_PALETTE = [ interface HistogramData { type: "Histogram"; - data: { x1: number; x2: number; y: number }[]; + data: { x1: number; x2: number; y0: number; y1: number; color: string }[]; domain: [number, number]; range: string[]; } +// Standard subtractive-light combinations used to render RGB's per-channel histograms as +// stacked, flat-colored bands (e.g. overlapping red and green bars become one yellow band) — +// the same result `mix-blend-mode: screen` produces, but precomputed so it doesn't depend on +// (and get washed out by) whatever's behind the chart. +const PURE_CHANNEL_COLORS: Record<"r" | "g" | "b", string> = { + r: "#ff0000", + g: "#00ff00", + b: "#0000ff", +}; + +const PAIR_CHANNEL_COLORS: Record = { + bg: "#00ffff", + br: "#ff00ff", + gr: "#ffff00", +}; + +const TRIPLE_CHANNEL_COLOR = "#ffffff"; + interface BarChartData { type: "Bar Chart"; data: { x: string; y: number }[]; @@ -78,14 +96,32 @@ function VisualizationPanel() { const extent = domain[1] - domain[0]; const step = domain.every(Number.isInteger) && extent <= 64 ? 1 : extent / buckets; + // Colors are resolved to literal values here (rather than left to Plot's shared `color` + // scale) so that this histogram can be composed alongside others (e.g. RGB's stacked + // per-channel bands) without needing a single shared domain-to-color mapping. + const colorScale = d3 + .scaleLinear() + .domain( + range.map( + (_, i) => domain[0] + (i * extent) / (range.length - 1 || 1), + ), + ) + .range(range) + .interpolate(d3.interpolateRgb); return { type: "Histogram", - data: new Array(buckets).fill(0).map((_, i) => ({ - x1: domain[0] + i * step, - x2: domain[0] + (i + 1) * step, - y: histogramData?.[i] ?? 0, - })), + data: new Array(buckets).fill(0).map((_, i) => { + const x1 = domain[0] + i * step; + + return { + x1, + x2: domain[0] + (i + 1) * step, + y0: 0, + y1: histogramData?.[i] ?? 0, + color: colorScale(x1), + }; + }), domain, range, }; @@ -93,6 +129,70 @@ function VisualizationPanel() { [], ); + const createRgbHistogramData = React.useCallback( + ( + channelCounts: [Uint32Array, Uint32Array, Uint32Array], + domain: [number, number], + ): HistogramData => { + const [rCounts, gCounts, bCounts] = channelCounts; + const buckets = rCounts.length; + const extent = domain[1] - domain[0]; + const step = + domain.every(Number.isInteger) && extent <= 64 ? 1 : extent / buckets; + + const data: HistogramData["data"] = []; + + for (let i = 0; i < buckets; i++) { + const x1 = domain[0] + i * step; + const x2 = domain[0] + (i + 1) * step; + const entries = ( + [ + ["r", rCounts[i] ?? 0], + ["g", gCounts[i] ?? 0], + ["b", bCounts[i] ?? 0], + ] as const + ) + .slice() + .sort((a, b) => a[1] - b[1]); + const [lo, mid, hi] = entries; + + // Stack up to three bands per bucket, from the ground up: the height all three + // channels share (white), then the height the top two share (their pairwise + // combination), then the remainder of the tallest channel alone (its pure color). + if (lo[1] > 0) { + data.push({ x1, x2, y0: 0, y1: lo[1], color: TRIPLE_CHANNEL_COLOR }); + } + if (mid[1] > lo[1]) { + const pairKey = [mid[0], hi[0]].sort().join(""); + data.push({ + x1, + x2, + y0: lo[1], + y1: mid[1], + color: PAIR_CHANNEL_COLORS[pairKey], + }); + } + if (hi[1] > mid[1]) { + data.push({ + x1, + x2, + y0: mid[1], + y1: hi[1], + color: PURE_CHANNEL_COLORS[hi[0]], + }); + } + } + + return { + type: "Histogram", + data, + domain, + range: [], + }; + }, + [], + ); + const { type, data, domain, range } = React.useMemo((): ChartData => { switch (render.renderMode) { case "Grayscale": { @@ -105,6 +205,31 @@ function VisualizationPanel() { ["#000000", "#ffffff"], ); } + case "RGB": { + const min = funcs[activeFunc].min_value ?? 0; + const max = funcs[activeFunc].max_value ?? 255; + const domain: [number, number] = [min, max]; + const bins = 256; + const numChannels = tabularData + ? Math.floor(tabularData.length / bins) + : 0; + + if (numChannels >= 3) { + return createRgbHistogramData( + [ + tabularData!.slice(0, bins), + tabularData!.slice(bins, bins * 2), + tabularData!.slice(bins * 2, bins * 3), + ], + domain, + ); + } + + return createHistogramData(tabularData ?? new Uint32Array(), domain, [ + "#000000", + "#ffffff", + ]); + } case "Store Frequency": { const min = scale === "log" ? 1 : 0; const max = @@ -192,6 +317,7 @@ function VisualizationPanel() { scale, thread.op, createHistogramData, + createRgbHistogramData, ]); const renderChart = React.useCallback(() => { diff --git a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx index cf463f8501b1..034875f0593b 100644 --- a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx +++ b/apps/halidoscope/src/components/controls/histogram/Histogram.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import type { Scale } from "@/state/tabularData"; interface HistogramProps { - data: { x1: number; x2: number; y: number }[]; + data: { x1: number; x2: number; y0: number; y1: number; color: string }[]; domain: [number, number]; range: string[]; scale: Scale; @@ -19,7 +19,7 @@ function Histogram({ data, domain, scale, range, labels }: HistogramProps) { const ref = React.useRef(null); React.useEffect(() => { - if (!ref.current) { + if (!ref.current || data.length === 0) { return; } @@ -36,7 +36,7 @@ function Histogram({ data, domain, scale, range, labels }: HistogramProps) { ticks: 8, }, x: { - domain: [data[0].x1, data[data.length - 1].x2], + domain, label: labels.x, labelAnchor: "right", labelArrow: "right", @@ -45,22 +45,32 @@ function Histogram({ data, domain, scale, range, labels }: HistogramProps) { tickSize: 0, type: scale, }, - color: { - domain, - range, - type: "linear", - interpolate: "rgb" as const, - }, marks: [ - Plot.rectY(data, { x1: "x1", x2: "x2", y: "y", fill: "x1" }), - Plot.ruleY(data, { - stroke: "x1", - strokeWidth: 8, + // Bars carry a literal, precomputed `color` (rather than relying on Plot's shared + // `color` scale), since RGB's stacked per-channel bands need per-band colors that a + // single domain-to-color mapping can't express. + Plot.rect(data, { x1: "x1", x2: "x2", - y: 0, - dy: 12, + y1: "y0", + y2: "y1", + fill: "color", }), + // RGB's stacked bands don't have one representative color per bucket, so the axis + // color strip below is only meaningful (and only supplied via `range`) for the + // single-series histograms. + ...(range.length > 0 + ? [ + Plot.ruleY(data, { + stroke: "color", + strokeWidth: 8, + x1: "x1", + x2: "x2", + y: 0, + dy: 12, + }), + ] + : []), ], }); @@ -71,7 +81,7 @@ function Histogram({ data, domain, scale, range, labels }: HistogramProps) { }; }, [data, domain, labels, range, scale]); - return data.every((d) => d.y === 0) ? ( + return data.every((d) => d.y1 === d.y0) ? (
- {render.renderMode !== "Grayscale" ? ( + {render.renderMode !== "Grayscale" && render.renderMode !== "RGB" ? (
{ @@ -127,6 +128,7 @@ export async function renderRgb({ func, globalIndex, normalizationMode, + includeTabularData, includeNan, includeInf, }); @@ -135,7 +137,7 @@ export async function renderRgb({ buffer, width, height, - includeTabularData: false, + includeTabularData, }); } From 5868c552885439aa2a40653fde7bc14fb17f1720 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Fri, 31 Jul 2026 10:34:17 -0700 Subject: [PATCH 37/67] Add .halidoscope method to Pipeline class. Co-authored-by: Claude Opus 5 --- .../src/halide/halide_/PyPipeline.cpp | 29 ++ src/Pipeline.cpp | 259 ++++++++++++++++++ src/Pipeline.h | 25 ++ 3 files changed, 313 insertions(+) diff --git a/python_bindings/src/halide/halide_/PyPipeline.cpp b/python_bindings/src/halide/halide_/PyPipeline.cpp index f9544c2f9f62..a76ea104fb2c 100644 --- a/python_bindings/src/halide/halide_/PyPipeline.cpp +++ b/python_bindings/src/halide/halide_/PyPipeline.cpp @@ -227,6 +227,35 @@ void define_pipeline(py::module &m) { }, py::arg("dst"), py::arg("target") = Target()) + // Development/debugging aid: see Pipeline::halidoscope() in Pipeline.h. + // Blocks until the Halidoscope window is closed, so the GIL must be + // released for the duration of the call, same as realize() above. + .def("halidoscope", // + [](Pipeline &p, Buffer<> buffer, const Target &target) -> void { + py::gil_scoped_release release; + p.halidoscope(Realization(std::move(buffer)), target); // + }, + py::arg("dst"), py::arg("target") = Target()) + + // See the comment on the corresponding realize() overload above: this + // overload must be declared before the list-of-sizes one, so that an + // empty list [] is resolved as list-of-sizes (a 0-dimensional Buffer) + // rather than as an ambiguous empty list-of-buffers. + .def("halidoscope", // + [](Pipeline &p, std::vector sizes, const Target &target) -> void { + py::gil_scoped_release release; + p.halidoscope(std::move(sizes), target); // + }, + py::arg("sizes") = std::vector{}, py::arg("target") = Target()) + + // This will actually allow a list-of-buffers as well as a tuple-of-buffers, but that's OK. + .def("halidoscope", // + [](Pipeline &p, std::vector> buffers, const Target &target) -> void { + py::gil_scoped_release release; + p.halidoscope(Realization(std::move(buffers)), target); // + }, + py::arg("dst"), py::arg("target") = Target()) + .def("infer_input_bounds", // [](Pipeline &p, const py::object &dst, const Target &target) -> void { const Target t = to_jit_target(target); diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index e935fd4852df..cfb3f18b3275 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -1,4 +1,10 @@ #include +#include +#include +#include +#include +#include +#include #include #include "Argument.h" @@ -16,6 +22,7 @@ #include "PrintLoopNest.h" #include "RealizationOrder.h" #include "Serialization.h" +#include "Util.h" #include "WasmExecutor.h" using namespace Halide::Internal; @@ -900,6 +907,258 @@ void Pipeline::trace_pipeline() { contents->trace_pipeline = true; } +namespace { + +// State for the custom_trace callbacks below. custom_trace is a plain C +// function pointer (see JITHandlers in JITModule.h), so it can't capture +// anything -- this mirrors the approach taken by +// test/performance/profiler.cpp. The pointers/target are not thread-safe to +// mutate, but Pipeline::halidoscope() is documented as non-reentrant (i.e. +// not to be called concurrently with itself), so that's fine; the trace +// writer below does still need to support *this* pipeline's own worker +// threads emitting trace events concurrently during a single run. +Target halidoscope_profile_target; +std::string *halidoscope_profile_json = nullptr; + +// Writes the same on-disk packet format as halide_default_trace's +// HL_TRACE_FILE path (src/runtime/tracing.cpp), but owns the output stream +// directly in host code instead of going through the runtime's env-var +// triggered, process-global file handle. That global (halide_trace_file) is +// cached per JIT-shared-runtime instance and halide_shutdown_trace() only +// ever resets it to a permanently-disabled state (0, not -1), so it cannot +// be safely reopened for a second trace within the same process -- writing +// packets ourselves sidesteps that entirely and works for any number of +// halidoscope() calls in one process. +std::ofstream *halidoscope_trace_stream = nullptr; +std::mutex halidoscope_trace_mutex; +std::atomic halidoscope_trace_next_id{1}; + +int32_t halidoscope_capture_trace(JITUserContext *, const halide_trace_event_t *e) { + // The return value becomes the parent_id of any trace events nested + // inside this one (see halide_default_trace's use of `my_id`), so this + // must be computed regardless of whether we're actually writing it out. + int32_t my_id = halidoscope_trace_next_id.fetch_add(1); + if (!halidoscope_trace_stream) { + return my_id; + } + + const bool is_load_or_store = (e->event == halide_trace_load || e->event == halide_trace_store); + uint32_t value_bytes = is_load_or_store ? (uint32_t)(e->lanes * e->type.bytes()) : 0; + uint32_t header_bytes = (uint32_t)sizeof(halide_trace_packet_t); + uint32_t coords_bytes = (uint32_t)e->dimensions * (uint32_t)sizeof(int32_t); + uint32_t name_bytes = (uint32_t)strlen(e->func) + 1; + uint32_t trace_tag_bytes = e->trace_tag ? (uint32_t)strlen(e->trace_tag) + 1 : 1; + uint32_t total_size_without_padding = header_bytes + value_bytes + coords_bytes + name_bytes + trace_tag_bytes; + uint32_t total_size = (total_size_without_padding + 3) & ~3u; + + std::vector buf(total_size, 0); + auto *packet = reinterpret_cast(buf.data()); + packet->size = total_size; + packet->event = e->event; + packet->parent_id = e->parent_id; + packet->dimensions = e->dimensions; + if (is_load_or_store) { + packet->value_index = e->value_index; + packet->type_code = (uint8_t)e->type.code; + packet->type_bits = (uint8_t)e->type.bits; + packet->lanes = (uint16_t)e->lanes; + } else { + packet->id = my_id; + packet->thread_id = (e->event == halide_trace_begin_parallel_task) ? e->thread_id : 0; + } + if (e->coordinates) { + memcpy(packet->coordinates(), e->coordinates, coords_bytes); + } + if (e->value) { + memcpy(packet->value(), e->value, value_bytes); + } + memcpy(packet->func(), e->func, name_bytes); + memcpy(packet->trace_tag(), e->trace_tag ? e->trace_tag : "", trace_tag_bytes); + + { + std::lock_guard lock(halidoscope_trace_mutex); + halidoscope_trace_stream->write(reinterpret_cast(buf.data()), total_size); + } + + return my_id; +} + +void halidoscope_append_func_stats(std::ostringstream &out, + const halide_profiler_func_stats &static_stats, + const halide_profiler_func_stats &live_stats) { + out << "{" + << "\"name\":\"" << static_stats.name << "\"," + << "\"parent\":" << static_stats.parent << "," + << "\"canonical_id\":" << static_stats.canonical_id << "," + << "\"kind\":" << (int)static_stats.kind << "," + << "\"buffer_func_id\":" << static_stats.buffer_func_id << "," + << "\"time_ns\":" << live_stats.time << "," + << "\"memory_current\":" << live_stats.memory_current << "," + << "\"memory_peak\":" << live_stats.memory_peak << "," + << "\"memory_total\":" << live_stats.memory_total << "," + << "\"stack_peak\":" << live_stats.stack_peak << "," + << "\"active_threads_numerator\":" << live_stats.active_threads_numerator << "," + << "\"active_threads_denominator\":" << live_stats.active_threads_denominator << "," + << "\"num_allocs\":" << live_stats.num_allocs + << "}"; +} + +// Fires on every trace event emitted by the profile run. We only care +// about halide_trace_end_pipeline, which fires from inside the pipeline +// call, while the halide_profiler_instance_state for this run is still +// alive (JITCache::finish_profiling resets profiler state immediately +// after Pipeline::realize returns, so this is the only place to snapshot +// it from). +int32_t halidoscope_capture_profile(JITUserContext *, const halide_trace_event_t *e) { + if (e->event != halide_trace_end_pipeline || !halidoscope_profile_json) { + return 0; + } + + using GetStateFn = halide_profiler_state *(*)(); + auto get_state = (GetStateFn)JITSharedRuntime::find_symbol(halidoscope_profile_target, "halide_profiler_get_state"); + if (!get_state) { + return 0; + } + + // halidoscope() only ever has one instrumented pipeline running at a + // time, so the head of the instance list is always ours. + halide_profiler_instance_state *inst = get_state()->instances; + if (!inst || !inst->pipeline_stats) { + return 0; + } + const halide_profiler_pipeline_stats &ps = *inst->pipeline_stats; + + std::ostringstream out; + out << "{\"pipelines\":[{" + << "\"name\":\"" << ps.name << "\"," + << "\"runs\":" << ps.runs << "," + << "\"billed_runs\":" << ps.billed_runs << "," + << "\"samples\":" << ps.samples << "," + << "\"num_allocs\":" << ps.num_allocs << "," + << "\"time_ns\":" << inst->billed_time << "," + << "\"memory_current\":" << inst->memory_current << "," + << "\"memory_peak\":" << inst->memory_peak << "," + << "\"memory_total\":" << inst->memory_total << "," + << "\"active_threads_numerator\":" << inst->active_threads_numerator << "," + << "\"active_threads_denominator\":" << inst->active_threads_denominator << "," + << "\"funcs\":["; + for (int i = 0; i < ps.num_funcs; i++) { + if (i > 0) { + out << ","; + } + halidoscope_append_func_stats(out, ps.funcs[i], inst->funcs[i]); + } + out << "]}]}"; + + *halidoscope_profile_json = out.str(); + return 0; +} + +// Builds a fresh, independent view over the same underlying output storage +// as `from`. RealizationArg is move-only and single-use by convention (its +// public realize() overload consumes it by value), but halidoscope() needs +// to realize into the same output twice (once per instrumented run; the +// results are discarded either way, so writing into the same storage twice +// is harmless). We can't just move `from` twice, so instead we copy out its +// (public) fields by hand. +Pipeline::RealizationArg halidoscope_clone_output(const Pipeline::RealizationArg &from) { + Pipeline::RealizationArg view(static_cast(nullptr)); + if (from.r) { + view.r = from.r; + } else if (from.buf) { + view.buf = from.buf; + } else if (from.buffer_list) { + view.buffer_list = std::make_unique>>(*from.buffer_list); + } + return view; +} + +} // namespace + +void Pipeline::halidoscope_impl(const std::function &do_realize, + const Target &target_arg) { + user_assert(defined()) << "Pipeline is undefined\n"; + + // Pipeline::compile_jit() discards the *entire* target (feature bits + // included) and replaces it with get_jit_target_from_environment() + // whenever has_unknowns() is true -- so an unresolved target_arg (e.g. + // the default Target()) would silently lose the trace/profile features + // we're about to add. Resolve it first so those features actually reach + // lowering/codegen. + Target base_target = target_arg.has_unknowns() ? get_jit_target_from_environment() : target_arg; + + std::map external_params; + std::vector data; + serialize_pipeline(*this, data, external_params); + + std::string dir = dir_make_temp(); + std::string trace_path = dir + "/trace.hltrace"; + std::string profile_path = dir + "/profile.json"; + + // --- Trace run: every Func's loads/stores/realizations, dumped to a + // binary trace file in the same format Halide's HL_TRACE_FILE path + // writes (see halidoscope_capture_trace above for why we write it + // ourselves instead of using HL_TRACE_FILE directly). --- + { + Pipeline traced = deserialize_pipeline(data, external_params); + traced.trace_pipeline(); + Target trace_target = base_target + .with_feature(Target::TraceLoads) + .with_feature(Target::TraceStores) + .with_feature(Target::TraceRealizations); + + std::ofstream trace_stream(trace_path, std::ios::binary); + user_assert(trace_stream.good()) << "halidoscope: unable to open " << trace_path << " for writing\n"; + halidoscope_trace_next_id = 1; + halidoscope_trace_stream = &trace_stream; + traced.jit_handlers().custom_trace = halidoscope_capture_trace; + + do_realize(traced, trace_target); + + halidoscope_trace_stream = nullptr; + trace_stream.close(); + } + + // --- Profile run: Halide's sampling profiler, captured into JSON. --- + { + Pipeline profiled = deserialize_pipeline(data, external_params); + profiled.trace_pipeline(); + Target profile_target = base_target.with_feature(Target::Profile); + + std::string profile_json; + halidoscope_profile_target = profile_target; + halidoscope_profile_json = &profile_json; + profiled.jit_handlers().custom_trace = halidoscope_capture_profile; + + do_realize(profiled, profile_target); + + halidoscope_profile_json = nullptr; + write_entire_file(profile_path, profile_json.data(), profile_json.size()); + } + + // --- Launch Halidoscope, blocking until the window is closed. --- + std::string binary = "halidoscope"; + if (const char *override_path = getenv("HALIDOSCOPE_PATH")) { + binary = override_path; + } + run_process({binary, "--trace", trace_path, "--profile", profile_path}); + + file_unlink(trace_path); + file_unlink(profile_path); + dir_rmdir(dir); +} + +void Pipeline::halidoscope(std::vector sizes, const Target &target) { + halidoscope_impl([&sizes](Pipeline &p, const Target &t) { p.realize(sizes, t); }, target); +} + +void Pipeline::halidoscope(RealizationArg output, const Target &target) { + halidoscope_impl([&output](Pipeline &p, const Target &t) { + p.realize(halidoscope_clone_output(output), t); + }, + target); +} + // Make a vector of void *'s to pass to the jit call using the // currently bound value for all of the params and image // params. diff --git a/src/Pipeline.h b/src/Pipeline.h index 0fa8b593eedf..a1932e554a19 100644 --- a/src/Pipeline.h +++ b/src/Pipeline.h @@ -514,8 +514,33 @@ class Pipeline { /** Generate begin_pipeline and end_pipeline tracing calls for this pipeline. */ void trace_pipeline(); + /** Development/debugging aid: run this pipeline twice under + * instrumentation (once with full tracing enabled, once with the + * profiler enabled), write the resulting trace and profile artifacts to + * a temporary directory, and open them in the Halidoscope GUI + * (https://github.com/halide/Halide, apps/halidoscope). The + * `halidoscope` executable is looked up on $PATH, unless the + * HALIDOSCOPE_PATH environment variable is set, in which case that path + * is used instead. + * + * This performs two additional realizations of the pipeline purely for + * the sake of instrumentation -- it does not realize the "real" output + * for the caller, and any output produced by these runs is discarded. + * This method blocks until the Halidoscope window is closed, at which + * point the temporary directory is deleted. + * + * Not reentrant/thread-safe; do not call this concurrently with itself + * or with another Halide JIT realization in the same process. */ + // @{ + void halidoscope(std::vector sizes = {}, const Target &target = Target()); + void halidoscope(RealizationArg output, const Target &target = Target()); + // @} + private: std::string generate_function_name() const; + + void halidoscope_impl(const std::function &do_realize, + const Target &target_arg); }; struct ExternSignature { From d809ffd82c7a52a49a07879dd9991fbd747f31da Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Mon, 3 Aug 2026 15:37:27 -0700 Subject: [PATCH 38/67] Support invoking Halidoscope with no arguments. Co-authored-by: Claude Opus 5 --- apps/halidoscope/package.json | 6 +- apps/halidoscope/pnpm-lock.yaml | 529 +++++++++++------- apps/halidoscope/src-tauri/Cargo.lock | 144 ++++- apps/halidoscope/src-tauri/Cargo.toml | 1 + .../src-tauri/capabilities/default.json | 3 +- apps/halidoscope/src-tauri/src/cli.rs | 8 +- apps/halidoscope/src-tauri/src/commands.rs | 25 +- apps/halidoscope/src-tauri/src/lib.rs | 29 +- apps/halidoscope/src-tauri/src/trace.rs | 21 +- apps/halidoscope/src-tauri/tauri.conf.json | 3 +- apps/halidoscope/src/App.tsx | 104 +++- .../Profiler.tsx => profile/Profile.tsx} | 8 +- .../views/{profiler => profile}/Treemap.tsx | 4 +- .../controls/ProfilerControlPanel.tsx | 0 .../src/components/views/trace/Trace.tsx | 24 + .../components/views/trace/TraceLoading.tsx | 21 + .../TraceTimeline.tsx} | 6 +- .../components/views/trace/TraceUpload.tsx | 39 ++ .../src/components/views/tracer/Tracer.tsx | 32 -- 19 files changed, 720 insertions(+), 287 deletions(-) rename apps/halidoscope/src/components/views/{profiler/Profiler.tsx => profile/Profile.tsx} (52%) rename apps/halidoscope/src/components/views/{profiler => profile}/Treemap.tsx (100%) rename apps/halidoscope/src/components/views/{profiler => profile}/controls/ProfilerControlPanel.tsx (100%) create mode 100644 apps/halidoscope/src/components/views/trace/Trace.tsx create mode 100644 apps/halidoscope/src/components/views/trace/TraceLoading.tsx rename apps/halidoscope/src/components/views/{tracer/TracerTimeline.tsx => trace/TraceTimeline.tsx} (97%) create mode 100644 apps/halidoscope/src/components/views/trace/TraceUpload.tsx delete mode 100644 apps/halidoscope/src/components/views/tracer/Tracer.tsx diff --git a/apps/halidoscope/package.json b/apps/halidoscope/package.json index 66aeb897bd30..5c8260db92fa 100644 --- a/apps/halidoscope/package.json +++ b/apps/halidoscope/package.json @@ -18,12 +18,14 @@ "@tailwindcss/vite": "^4.3.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-cli": "^2.4.1", + "@tauri-apps/plugin-dialog": "~2.7.2", "@tauri-apps/plugin-opener": "^2", "@xyflow/react": "^12.11.0", "clsx": "^2.1.1", "d3": "^7.9.0", "jotai": "^2.20.1", "lodash-es": "^4.18.1", + "motion": "^12.43.0", "radix-ui": "^1.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -36,13 +38,13 @@ "@types/lodash-es": "^4.17.12", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.6.0", + "@vitejs/plugin-react": "^6.0.5", "eslint": "^10.4.1", "eslint-plugin-react-hooks": "^7.1.1", "prettier": "^3.8.4", "prettier-plugin-tailwindcss": "^0.8.0", "typescript": "~5.8.3", "typescript-eslint": "^8.60.1", - "vite": "^8.0.16" + "vite": "^8.2.0" } } diff --git a/apps/halidoscope/pnpm-lock.yaml b/apps/halidoscope/pnpm-lock.yaml index 0d1fe3953fac..31c18383c79d 100644 --- a/apps/halidoscope/pnpm-lock.yaml +++ b/apps/halidoscope/pnpm-lock.yaml @@ -16,13 +16,16 @@ importers: version: 0.6.17 '@tailwindcss/vite': specifier: ^4.3.0 - version: 4.3.0(vite@8.0.16(jiti@2.7.0)) + version: 4.3.0(vite@8.2.0(jiti@2.7.0)) '@tauri-apps/api': specifier: ^2 version: 2.11.0 '@tauri-apps/plugin-cli': specifier: ^2.4.1 version: 2.4.1 + '@tauri-apps/plugin-dialog': + specifier: ~2.7.2 + version: 2.7.2 '@tauri-apps/plugin-opener': specifier: ^2 version: 2.5.4 @@ -41,6 +44,9 @@ importers: lodash-es: specifier: ^4.18.1 version: 4.18.1 + motion: + specifier: ^12.43.0 + version: 12.43.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -73,8 +79,8 @@ importers: specifier: ^19.1.6 version: 19.2.3(@types/react@19.2.15) '@vitejs/plugin-react': - specifier: ^4.6.0 - version: 4.7.0(vite@8.0.16(jiti@2.7.0)) + specifier: ^6.0.5 + version: 6.0.5(vite@8.2.0(jiti@2.7.0)) eslint: specifier: ^10.4.1 version: 10.4.1(jiti@2.7.0) @@ -94,8 +100,8 @@ importers: specifier: ^8.60.1 version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.8.3) vite: - specifier: ^8.0.16 - version: 8.0.16(jiti@2.7.0) + specifier: ^8.2.0 + version: 8.2.0(jiti@2.7.0) packages: @@ -133,10 +139,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.29.7': - resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -158,18 +160,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-transform-react-jsx-self@7.29.7': - resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.29.7': - resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -188,14 +178,14 @@ packages: '@dagrejs/graphlib@4.0.1': resolution: {integrity: sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@2.0.0-alpha.3': + resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@2.0.0-alpha.3': + resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@2.0.1': + resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} @@ -287,18 +277,19 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.2.2': + resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 '@observablehq/plot@0.6.17': resolution: {integrity: sha512-/qaXP/7mc4MUS0s4cPPFASDRjtsWp85/TbfsciqDgU1HwYixbSbbytNuInD8AcTYC3xaxACgVX06agdfQy9W+g==} engines: {node: '>=12'} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -990,104 +981,100 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.2.1': + resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.2.1': + resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.2.1': + resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.2.1': + resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.2.1': + resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.2.1': + resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.1': + resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.2.1': + resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.2.1': + resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.2.1': + resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.2.1': + resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.2.1': + resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.2.1': + resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.2.1': + resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-beta.27': - resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -1267,23 +1254,14 @@ packages: '@tauri-apps/plugin-cli@2.4.1': resolution: {integrity: sha512-8JXofQFI5cmiGolh1PlU4hzE2YJgrgB1lyaztyBYiiMCy13luVxBXaXChYPeqMkUo46J1UadxvYdjRjj0E8zaw==} + '@tauri-apps/plugin-dialog@2.7.2': + resolution: {integrity: sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg==} + '@tauri-apps/plugin-opener@2.5.4': resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1463,11 +1441,18 @@ packages: resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitejs/plugin-react@4.7.0': - resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} - engines: {node: ^14.18.0 || >=16.0.0} + '@vitejs/plugin-react@6.0.5': + resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} + engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true '@xyflow/react@12.11.0': resolution: {integrity: sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==} @@ -1792,6 +1777,20 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + framer-motion@12.43.0: + resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1912,30 +1911,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -1943,6 +1972,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -1950,6 +1986,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -1957,6 +2000,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -1964,22 +2014,45 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1997,11 +2070,31 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + motion-dom@12.43.0: + resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + + motion@12.43.0: + resolution: {integrity: sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2039,8 +2132,12 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -2129,10 +2226,6 @@ packages: peerDependencies: react: ^19.2.6 - react-refresh@0.17.0: - resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} - engines: {node: '>=0.10.0'} - react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -2170,8 +2263,8 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.2.1: + resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2275,13 +2368,13 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -2422,8 +2515,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} @@ -2439,16 +2530,6 @@ snapshots: dependencies: '@babel/types': 7.29.7 - '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - - '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -2478,18 +2559,18 @@ snapshots: '@dagrejs/graphlib@4.0.1': {} - '@emnapi/core@1.10.0': + '@emnapi/core@2.0.0-alpha.3': dependencies: - '@emnapi/wasi-threads': 1.2.1 + '@emnapi/wasi-threads': 2.0.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': + '@emnapi/runtime@2.0.0-alpha.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': + '@emnapi/wasi-threads@2.0.1': dependencies: tslib: 2.8.1 optional: true @@ -2580,11 +2661,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@tybys/wasm-util': 0.10.3 optional: true '@observablehq/plot@0.6.17': @@ -2593,7 +2674,7 @@ snapshots: interval-tree-1d: 1.0.4 isoformat: 0.2.1 - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.142.0': {} '@radix-ui/number@1.1.1': {} @@ -3342,57 +3423,55 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.2.1': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.2.1': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.2.1': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.2.1': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.2.1': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.2.1': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.2.1': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.2.1': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.2.1': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.2.1': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.2.1': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.2.1': optional: true - '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rolldown/pluginutils@1.0.1': {} '@tailwindcss/node@4.3.0': @@ -3456,12 +3535,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - '@tailwindcss/vite@4.3.0(vite@8.0.16(jiti@2.7.0))': + '@tailwindcss/vite@4.3.0(vite@8.2.0(jiti@2.7.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 8.0.16(jiti@2.7.0) + vite: 8.2.0(jiti@2.7.0) '@tauri-apps/api@2.11.0': {} @@ -3516,36 +3595,19 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.0 + '@tauri-apps/plugin-dialog@2.7.2': + dependencies: + '@tauri-apps/api': 2.11.0 + '@tauri-apps/plugin-opener@2.5.4': dependencies: '@tauri-apps/api': 2.11.0 - '@tybys/wasm-util@0.10.2': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.29.7 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.29.7 - '@types/d3-array@3.2.2': {} '@types/d3-axis@3.0.6': @@ -3776,17 +3838,10 @@ snapshots: '@typescript-eslint/types': 8.60.1 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@4.7.0(vite@8.0.16(jiti@2.7.0))': + '@vitejs/plugin-react@6.0.5(vite@8.2.0(jiti@2.7.0))': dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) - '@rolldown/pluginutils': 1.0.0-beta.27 - '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: 8.0.16(jiti@2.7.0) - transitivePeerDependencies: - - supports-color + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.0(jiti@2.7.0) '@xyflow/react@12.11.0(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -4146,6 +4201,15 @@ snapshots: flatted@3.4.2: {} + framer-motion@12.43.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + motion-dom: 12.43.0 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + fsevents@2.3.3: optional: true @@ -4224,36 +4288,69 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -4270,6 +4367,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4288,9 +4401,23 @@ snapshots: dependencies: brace-expansion: 5.0.6 + motion-dom@12.43.0: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + + motion@12.43.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + framer-motion: 12.43.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + ms@2.1.3: {} - nanoid@3.3.12: {} + nanoid@3.3.16: {} natural-compare@1.4.0: {} @@ -4321,9 +4448,11 @@ snapshots: picomatch@4.0.4: {} - postcss@8.5.15: + picomatch@4.0.5: {} + + postcss@8.5.25: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -4405,8 +4534,6 @@ snapshots: react: 19.2.6 scheduler: 0.27.0 - react-refresh@0.17.0: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.15)(react@19.2.6): dependencies: react: 19.2.6 @@ -4438,26 +4565,26 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.0.3: + rolldown@1.2.1: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.142.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.2.1 + '@rolldown/binding-darwin-arm64': 1.2.1 + '@rolldown/binding-darwin-x64': 1.2.1 + '@rolldown/binding-freebsd-x64': 1.2.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.1 + '@rolldown/binding-linux-arm64-gnu': 1.2.1 + '@rolldown/binding-linux-arm64-musl': 1.2.1 + '@rolldown/binding-linux-ppc64-gnu': 1.2.1 + '@rolldown/binding-linux-s390x-gnu': 1.2.1 + '@rolldown/binding-linux-x64-gnu': 1.2.1 + '@rolldown/binding-linux-x64-musl': 1.2.1 + '@rolldown/binding-openharmony-arm64': 1.2.1 + '@rolldown/binding-wasm32-wasi': 1.2.1 + '@rolldown/binding-win32-arm64-msvc': 1.2.1 + '@rolldown/binding-win32-x64-msvc': 1.2.1 rw@1.3.3: {} @@ -4538,12 +4665,12 @@ snapshots: dependencies: react: 19.2.6 - vite@8.0.16(jiti@2.7.0): + vite@8.2.0(jiti@2.7.0): dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.1 tinyglobby: 0.2.17 optionalDependencies: fsevents: 2.3.3 diff --git a/apps/halidoscope/src-tauri/Cargo.lock b/apps/halidoscope/src-tauri/Cargo.lock index 72d73ba29597..935010640855 100644 --- a/apps/halidoscope/src-tauri/Cargo.lock +++ b/apps/halidoscope/src-tauri/Cargo.lock @@ -1567,6 +1567,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-cli", + "tauri-plugin-dialog", "tauri-plugin-opener", ] @@ -2398,6 +2399,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.12.1", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -3018,6 +3020,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3716,6 +3742,48 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.4" @@ -4833,6 +4901,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4866,13 +4943,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -4903,6 +4997,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4915,6 +5015,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4927,12 +5033,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4945,6 +5063,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -4957,6 +5081,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4969,6 +5099,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -4981,6 +5117,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" diff --git a/apps/halidoscope/src-tauri/Cargo.toml b/apps/halidoscope/src-tauri/Cargo.toml index 8b14e7b4618a..2456437574ee 100644 --- a/apps/halidoscope/src-tauri/Cargo.toml +++ b/apps/halidoscope/src-tauri/Cargo.toml @@ -25,6 +25,7 @@ serde_json = "1" comfy-table = "7" palette = "0.7.6" colorous = "1.0.16" +tauri-plugin-dialog = "2" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-cli = "2.0.0" diff --git a/apps/halidoscope/src-tauri/capabilities/default.json b/apps/halidoscope/src-tauri/capabilities/default.json index ceb4a9c62805..b4320abb459a 100644 --- a/apps/halidoscope/src-tauri/capabilities/default.json +++ b/apps/halidoscope/src-tauri/capabilities/default.json @@ -8,6 +8,7 @@ "permissions": [ "core:default", "opener:default", - "cli:default" + "cli:default", + "dialog:default" ] } diff --git a/apps/halidoscope/src-tauri/src/cli.rs b/apps/halidoscope/src-tauri/src/cli.rs index 564e2c1cdc54..26b84dc8d1c3 100644 --- a/apps/halidoscope/src-tauri/src/cli.rs +++ b/apps/halidoscope/src-tauri/src/cli.rs @@ -34,7 +34,7 @@ fn dot(subcommand: Box) -> Option<()> { let destination = args.get("destination").and_then(|a| a.value.as_str()); // Load and parse the trace. - let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + let trace = Trace::load_from_file(trace_path, |_| {}).unwrap_or_else(|e| { eprintln!("Error loading trace: {}", e); std::process::exit(1); }); @@ -75,7 +75,7 @@ fn list(subcommand: Box) -> Option<()> { let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; // Load and parse the trace. - let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + let trace = Trace::load_from_file(trace_path, |_| {}).unwrap_or_else(|e| { eprintln!("Error loading trace: {}", e); std::process::exit(1); }); @@ -129,7 +129,7 @@ fn stats(subcommand: Box) -> Option<()> { let func = args.get("func").and_then(|a| a.value.as_str()); // Load and parse the trace. - let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + let trace = Trace::load_from_file(trace_path, |_| {}).unwrap_or_else(|e| { eprintln!("Error loading trace: {}", e); std::process::exit(1); }); @@ -230,7 +230,7 @@ fn snapshot(subcommand: Box) -> Option<()> { let destination = args.get("destination").and_then(|a| a.value.as_str())?; // Load and parse the trace. - let trace = Trace::load_from_file(trace_path).unwrap_or_else(|e| { + let trace = Trace::load_from_file(trace_path, |_| {}).unwrap_or_else(|e| { eprintln!("Error loading trace: {}", e); std::process::exit(1); }); diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 40417f855a79..1ecffbe4ecd9 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -7,7 +7,7 @@ use std::sync::Mutex; use serde::{Deserialize, Serialize}; use tauri::ipc::Response; -use tauri::State; +use tauri::{AppHandle, Emitter, State}; use crate::render::{ GrayscaleState, IncludeInf, IncludeNan, LoadFrequencyState, NormalizationMode, RedundantState, @@ -215,10 +215,27 @@ fn pack_render_response( /// Parses a `.hltrace` file and returns the metadata the frontend needs to set up canvases and /// the scrub timeline. Replaces any previously loaded trace. +/// +/// Runs the parse on a blocking-task thread rather than the main thread: `open_trace` isn't +/// declared `async`, so a plain `fn` command would otherwise execute inline on the thread that +/// pumps the webview's event loop, freezing the UI (including any in-progress loading indicator) +/// for the duration of the parse. Progress (percentage of bytes parsed) is emitted to the +/// frontend as `trace-load-progress` events. #[tauri::command] -pub fn open_trace(path: String, state: State) -> Result { - let trace = Trace::load_from_file(&path)?; - let meta = TraceMeta::from_trace(&trace); +pub async fn open_trace( + path: String, + app: AppHandle, + state: State<'_, AppState>, +) -> Result { + let (trace, meta) = tauri::async_runtime::spawn_blocking(move || { + let trace = Trace::load_from_file(&path, |pct| { + let _ = app.emit("trace-load-progress", pct); + })?; + let meta = TraceMeta::from_trace(&trace); + Ok::<_, String>((trace, meta)) + }) + .await + .map_err(|e| e.to_string())??; let mut guard = state.inner.lock().map_err(|e| e.to_string())?; *guard = Some(Loaded { diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index 07662cb93b57..f704404f17c1 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -28,19 +28,30 @@ fn get_cwd() -> Result { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_cli::init()) .setup(|app| { match app.cli().matches() { - Ok(matches) => match matches.subcommand { - Some(subcommand) => halidoscope_cli(subcommand), - None => { - tauri::WebviewWindowBuilder::from_config( - app.handle(), - &app.config().app.windows[0], - )? - .build()?; + Ok(matches) => { + if let Some(help) = matches.args.get("help").and_then(|a| a.value.as_str()) { + println!("{}", help); + std::process::exit(0); } - }, + if matches.args.contains_key("version") { + println!("{}", app.package_info().version); + std::process::exit(0); + } + match matches.subcommand { + Some(subcommand) => halidoscope_cli(subcommand), + None => { + tauri::WebviewWindowBuilder::from_config( + app.handle(), + &app.config().app.windows[0], + )? + .build()?; + } + } + } Err(e) => { eprintln!("Error parsing CLI arguments: {}", e); std::process::exit(1); diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 33ae8fc0806e..41a431f7b34f 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -414,14 +414,19 @@ fn parse_func_type_and_dim( // ── Trace loading ───────────────────────────────────────────────────────────── impl Trace { - pub fn load_from_file(path: &str) -> Result { + pub fn load_from_file(path: &str, on_progress: impl FnMut(u8)) -> Result { let data = std::fs::read(path).map_err(|e| e.to_string())?; - Self::load_from_bytes(&data) + Self::load_from_bytes(&data, on_progress) } - pub fn load_from_bytes(data: &[u8]) -> Result { + /// Parses `data` into a `Trace`, invoking `on_progress` with the percentage (0-100) of bytes + /// consumed each time it crosses a new integer percentage point. Progress tracks bytes + /// consumed rather than packet count since the total packet count isn't known until parsing + /// completes (packets are variable-length). + pub fn load_from_bytes(data: &[u8], mut on_progress: impl FnMut(u8)) -> Result { let total = data.len(); let mut pos = 0; + let mut last_reported_pct: u8 = 0; let mut packets: Vec = Vec::new(); let mut funcs: BTreeMap = BTreeMap::new(); @@ -663,6 +668,14 @@ impl Trace { packets.push(pkt); pos += size; + + // Intentionally max pct at 95 to support movement to 100 after all stats below are + // computed. + let pct = (pos as u64 * 100 / total.max(1) as u64).max(95) as u8; + if pct > last_reported_pct { + last_reported_pct = pct; + on_progress(pct); + } } // ── DAG inference ──────────────────────────────────────────────────────────────────────── @@ -986,6 +999,8 @@ impl Trace { } } + on_progress(100); + Ok(Self { packets, funcs, diff --git a/apps/halidoscope/src-tauri/tauri.conf.json b/apps/halidoscope/src-tauri/tauri.conf.json index 9cab6a6b6e73..34025e8d645b 100644 --- a/apps/halidoscope/src-tauri/tauri.conf.json +++ b/apps/halidoscope/src-tauri/tauri.conf.json @@ -40,8 +40,7 @@ "name": "trace", "short": "t", "takesValue": true, - "description": "Path to .hltrace file to load on startup.", - "required": true + "description": "Path to .hltrace file to load on startup." }, { "name": "profile", diff --git a/apps/halidoscope/src/App.tsx b/apps/halidoscope/src/App.tsx index c7e74531e634..5fdb54fa4f5b 100644 --- a/apps/halidoscope/src/App.tsx +++ b/apps/halidoscope/src/App.tsx @@ -1,15 +1,18 @@ import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; import { getMatches } from "@tauri-apps/plugin-cli"; import { useSetAtom } from "jotai"; import { Tabs } from "radix-ui"; import * as React from "react"; -import Profiler from "@/components/views/profiler/Profiler"; -import Tracer from "@/components/views/tracer/Tracer"; +import Profile from "@/components/views/profile/Profile"; +import Trace from "@/components/views/trace/Trace"; +import TraceUpload from "@/components/views/trace/TraceUpload"; +import TraceLoading from "@/components/views/trace/TraceLoading"; import { ProfileContextProvider } from "@/hooks/profile"; import { TraceContextProvider } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; -import { Profile, type FuncMeta, type StatsMeta } from "@/types"; +import type { Profile as Pfile, FuncMeta, StatsMeta } from "@/types"; import { openProfile, openTrace } from "@/utils/api"; import "./App.css"; @@ -20,7 +23,31 @@ async function resolvePath(path: string) { : `${await invoke("get_cwd")}/${path}`; } +class TracePathError extends Error { + public readonly name: string; + + constructor(message: string) { + super(message); + + this.name = "TracePathError"; + Object.setPrototypeOf(this, TracePathError.prototype); + } +} + +enum TraceLoadingState { + Loading, + Loaded, + NeedsUpload, +} + function App() { + // Loading state. + const [traceLoading, setTraceLoading] = React.useState<{ + state: TraceLoadingState; + progress: number; + }>({ state: TraceLoadingState.Loading, progress: 0 }); + + // Trace state. const [funcs, setFuncs] = React.useState>({}); const [dagEdges, setDagEdges] = React.useState>({}); const [packetCount, setPacketCount] = React.useState(0); @@ -31,23 +58,24 @@ function App() { global_max_reuse_distance: 0, global_thread_ids: [], }); - const [profile, setProfile] = React.useState(null); + // Profile state. + const [profile, setProfile] = React.useState(null); + + // GUI state. const setActiveFunc = useSetAtom(funcAtom); - React.useEffect(() => { - async function loadTraceFromCLI() { - const matches = await getMatches(); - const tracePath = matches.args.trace?.value; + const loadTrace = React.useCallback( + async (path: string) => { + setTraceLoading({ state: TraceLoadingState.Loading, progress: 0 }); - if (typeof tracePath !== "string") { - return; - } - const resolvedTracePath = await resolvePath(tracePath); + const unlisten = await listen("trace-load-progress", (event) => { + setTraceLoading((prev) => ({ ...prev, progress: event.payload })); + }); try { const { funcs, total_packets, dag_edges, stats } = - await openTrace(resolvedTracePath); + await openTrace(path); const byName: Record = {}; for (const func of funcs) { @@ -59,13 +87,42 @@ function App() { setPacketCount(total_packets); setStats(stats); setActiveFunc(funcs[0]?.name ?? ""); + } finally { + unlisten(); + setTraceLoading({ state: TraceLoadingState.Loaded, progress: 100 }); + } + }, + [setActiveFunc], + ); + + React.useEffect(() => { + async function loadTraceFromCLI() { + try { + const matches = await getMatches(); + const tracePath = matches.args.trace?.value; + + if (typeof tracePath !== "string") { + throw new TracePathError( + `Unexpected value for trace path: ${tracePath}`, + ); + } + + const resolvedTracePath = await resolvePath(tracePath); + await loadTrace(resolvedTracePath); } catch (err) { - console.error("Error loading trace from CLI: ", err); + if (err instanceof TracePathError) { + setTraceLoading((prev) => ({ + ...prev, + state: TraceLoadingState.NeedsUpload, + })); + } else { + console.error("Error loading trace: ", err); + } } } loadTraceFromCLI(); - }, [setActiveFunc]); + }, [loadTrace]); React.useEffect(() => { async function loadProfileFromCLI() { @@ -88,6 +145,17 @@ function App() { loadProfileFromCLI(); }, []); + const renderTrace = React.useCallback(() => { + switch (traceLoading.state) { + case TraceLoadingState.Loading: + return ; + case TraceLoadingState.NeedsUpload: + return ; + case TraceLoadingState.Loaded: + return ; + } + }, [traceLoading, loadTrace]); + return ( @@ -115,16 +183,14 @@ function App() { stats, }} > -
- -
+
{renderTrace()}
{profile !== null ? (
- +
diff --git a/apps/halidoscope/src/components/views/profiler/Profiler.tsx b/apps/halidoscope/src/components/views/profile/Profile.tsx similarity index 52% rename from apps/halidoscope/src/components/views/profiler/Profiler.tsx rename to apps/halidoscope/src/components/views/profile/Profile.tsx index 36feefec2106..ce2499b3a1ec 100644 --- a/apps/halidoscope/src/components/views/profiler/Profiler.tsx +++ b/apps/halidoscope/src/components/views/profile/Profile.tsx @@ -1,7 +1,7 @@ -import ProfilerControlPanel from "@/components/views/profiler/controls/ProfilerControlPanel"; -import Treemap from "@/components/views/profiler/Treemap"; +import ProfilerControlPanel from "@/components/views/profile/controls/ProfilerControlPanel"; +import Treemap from "@/components/views/profile/Treemap"; -function Profiler() { +function Profile() { return (
@@ -12,4 +12,4 @@ function Profiler() { ); } -export default Profiler; +export default Profile; diff --git a/apps/halidoscope/src/components/views/profiler/Treemap.tsx b/apps/halidoscope/src/components/views/profile/Treemap.tsx similarity index 100% rename from apps/halidoscope/src/components/views/profiler/Treemap.tsx rename to apps/halidoscope/src/components/views/profile/Treemap.tsx index 7dba720c2b01..bab2400b8ea9 100644 --- a/apps/halidoscope/src/components/views/profiler/Treemap.tsx +++ b/apps/halidoscope/src/components/views/profile/Treemap.tsx @@ -1,12 +1,12 @@ import { clsx } from "clsx"; import * as d3 from "d3"; +import { useAtomValue } from "jotai"; import { Tooltip } from "radix-ui"; import * as React from "react"; import { useProfileContext } from "@/hooks/profile"; -import type { Profile } from "@/types"; import { profileMetricAtom, type ProfileMetric } from "@/state/profile-metric"; -import { useAtomValue } from "jotai"; +import type { Profile } from "@/types"; type TreemapNode = { name: string; diff --git a/apps/halidoscope/src/components/views/profiler/controls/ProfilerControlPanel.tsx b/apps/halidoscope/src/components/views/profile/controls/ProfilerControlPanel.tsx similarity index 100% rename from apps/halidoscope/src/components/views/profiler/controls/ProfilerControlPanel.tsx rename to apps/halidoscope/src/components/views/profile/controls/ProfilerControlPanel.tsx diff --git a/apps/halidoscope/src/components/views/trace/Trace.tsx b/apps/halidoscope/src/components/views/trace/Trace.tsx new file mode 100644 index 000000000000..4470686da2e5 --- /dev/null +++ b/apps/halidoscope/src/components/views/trace/Trace.tsx @@ -0,0 +1,24 @@ +import { ReactFlowProvider } from "@xyflow/react"; + +import Canvas from "@/components/canvas/Canvas"; +import TraceTimeline from "@/components/views/trace/TraceTimeline"; +import { useTraceContext } from "@/hooks/trace"; +import ControlTabs from "@/components/controls/ControlTabs"; + +function Trace() { + const { funcs, dagEdges, packetCount } = useTraceContext(); + + return ( +
+
+ + + + +
+ +
+ ); +} + +export default Trace; diff --git a/apps/halidoscope/src/components/views/trace/TraceLoading.tsx b/apps/halidoscope/src/components/views/trace/TraceLoading.tsx new file mode 100644 index 000000000000..5114a4b0ecb0 --- /dev/null +++ b/apps/halidoscope/src/components/views/trace/TraceLoading.tsx @@ -0,0 +1,21 @@ +import { motion } from "motion/react"; + +interface Props { + progress: number; +} + +function TraceLoading({ progress }: Props) { + return ( +
+

Loading trace...

+
+ +
+
+ ); +} + +export default TraceLoading; diff --git a/apps/halidoscope/src/components/views/tracer/TracerTimeline.tsx b/apps/halidoscope/src/components/views/trace/TraceTimeline.tsx similarity index 97% rename from apps/halidoscope/src/components/views/tracer/TracerTimeline.tsx rename to apps/halidoscope/src/components/views/trace/TraceTimeline.tsx index d8946197643d..bd2e0134ee7c 100644 --- a/apps/halidoscope/src/components/views/tracer/TracerTimeline.tsx +++ b/apps/halidoscope/src/components/views/trace/TraceTimeline.tsx @@ -7,11 +7,11 @@ import { packetAtom } from "@/state/packet"; import { playbackRateAtom } from "@/state/playback"; import { SCRUB_DEBOUNCE_MS } from "@/utils/constants"; -interface TracerTimelineProps { +interface Props { packetCount: number; } -function TracerTimeline({ packetCount }: TracerTimelineProps) { +function TraceTimeline({ packetCount }: Props) { // Local slider position, for a smooth thumb independent of render cadence. const [packetIndex, setPacketIndex] = React.useState(0); const [playing, setPlaying] = React.useState(false); @@ -168,4 +168,4 @@ function TracerTimeline({ packetCount }: TracerTimelineProps) { ); } -export default TracerTimeline; +export default TraceTimeline; diff --git a/apps/halidoscope/src/components/views/trace/TraceUpload.tsx b/apps/halidoscope/src/components/views/trace/TraceUpload.tsx new file mode 100644 index 000000000000..df98dda090dd --- /dev/null +++ b/apps/halidoscope/src/components/views/trace/TraceUpload.tsx @@ -0,0 +1,39 @@ +import { open } from "@tauri-apps/plugin-dialog"; + +interface Props { + onUpload: (path: string) => void; +} + +function TraceUpload({ onUpload }: Props) { + async function handleClick() { + const path = await open({ + multiple: false, + filters: [{ name: "Halide Trace", extensions: ["hltrace"] }], + }); + + if (!path) { + return; + } + + onUpload(path); + } + + return ( +
+ +
+ ); +} + +export default TraceUpload; diff --git a/apps/halidoscope/src/components/views/tracer/Tracer.tsx b/apps/halidoscope/src/components/views/tracer/Tracer.tsx deleted file mode 100644 index 6b5c415d5692..000000000000 --- a/apps/halidoscope/src/components/views/tracer/Tracer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ReactFlowProvider } from "@xyflow/react"; - -import Canvas from "@/components/canvas/Canvas"; -import Timeline from "@/components/views/tracer/TracerTimeline"; -import { useTraceContext } from "@/hooks/trace"; -import ControlTabs from "@/components/controls/ControlTabs"; - -function Tracer() { - const { funcs, dagEdges, packetCount } = useTraceContext(); - - return ( -
-
- {Object.keys(funcs).length > 0 ? ( - <> - - - - - - ) : ( -
-

Loading trace...

-
- )} -
- -
- ); -} - -export default Tracer; From 7cc5bb6ba0ae723c55d69b90e284ab6213590c06 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 4 Aug 2026 14:04:57 -0700 Subject: [PATCH 39/67] Apply clippy recommended fixes. --- apps/halidoscope/src-tauri/src/cli.rs | 14 +++++++------- apps/halidoscope/src-tauri/src/commands.rs | 9 +++------ apps/halidoscope/src-tauri/src/graph.rs | 2 +- apps/halidoscope/src-tauri/src/lib.rs | 2 +- apps/halidoscope/src-tauri/src/render.rs | 8 +++----- apps/halidoscope/src-tauri/src/trace.rs | 18 ++---------------- 6 files changed, 17 insertions(+), 36 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/cli.rs b/apps/halidoscope/src-tauri/src/cli.rs index 26b84dc8d1c3..675aac9569ca 100644 --- a/apps/halidoscope/src-tauri/src/cli.rs +++ b/apps/halidoscope/src-tauri/src/cli.rs @@ -14,7 +14,7 @@ use crate::render::{ }; use crate::trace::Trace; -pub fn halidoscope_cli(subcommand: Box) { +pub fn halidoscope_cli(subcommand: SubcommandMatches) { match subcommand.name.as_str() { "dot" => dot(subcommand), "list" => list(subcommand), @@ -27,7 +27,7 @@ pub fn halidoscope_cli(subcommand: Box) { }; } -fn dot(subcommand: Box) -> Option<()> { +fn dot(subcommand: SubcommandMatches) -> Option<()> { let args = &subcommand.matches.args; let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; @@ -69,7 +69,7 @@ fn dot(subcommand: Box) -> Option<()> { } } -fn list(subcommand: Box) -> Option<()> { +fn list(subcommand: SubcommandMatches) -> Option<()> { let args = &subcommand.matches.args; let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; @@ -122,7 +122,7 @@ fn list(subcommand: Box) -> Option<()> { std::process::exit(0); } -fn stats(subcommand: Box) -> Option<()> { +fn stats(subcommand: SubcommandMatches) -> Option<()> { let args = &subcommand.matches.args; let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; @@ -213,7 +213,7 @@ fn stats(subcommand: Box) -> Option<()> { std::process::exit(0); } -fn snapshot(subcommand: Box) -> Option<()> { +fn snapshot(subcommand: SubcommandMatches) -> Option<()> { let args = &subcommand.matches.args; let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; @@ -249,8 +249,8 @@ fn snapshot(subcommand: Box) -> Option<()> { } let ext = Path::new(destination).extension().and_then(OsStr::to_str); - let store_indices = trace.func_store_indices(&func)?; - let load_indices = trace.func_load_indices(&func)?; + let store_indices = trace.func_store_indices(func)?; + let load_indices = trace.func_load_indices(func)?; let k = packet_index.try_into().unwrap_or_else(|_| { eprintln!("Packet index {} is too large.", packet_index); std::process::exit(1); diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 1ecffbe4ecd9..ca4f850de0cd 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -115,10 +115,7 @@ impl TraceMeta { max_redundant_store_count: stats.max_redundant_store_count, max_reuse_distance: stats.max_reuse_distance, buffer_liveness: IndexRange::from_tuple( - trace - .func_buffer_liveness_range(name) - .unwrap_or(&(0, 0)) - .clone(), + *trace.func_buffer_liveness_range(name).unwrap_or(&(0, 0)), ), produce_ranges: trace .func_produce_ranges(name) @@ -142,8 +139,8 @@ impl TraceMeta { .map_or(1, |ids| ids.len() as u32), thread_ids: trace .func_thread_ids(name) - .map(|ids| ids.into_iter().map(|x| x.to_string()).collect()) - .unwrap_or_else(|| vec![]), + .map(|ids| ids.iter().map(|x| x.to_string()).collect()) + .unwrap_or_default(), } }) .collect(); diff --git a/apps/halidoscope/src-tauri/src/graph.rs b/apps/halidoscope/src-tauri/src/graph.rs index 44b2670246d7..c4843aabbe22 100644 --- a/apps/halidoscope/src-tauri/src/graph.rs +++ b/apps/halidoscope/src-tauri/src/graph.rs @@ -6,7 +6,7 @@ pub fn to_dot(dag_edges: &BTreeMap>) -> String { for (key, value) in dag_edges.iter() { for dest in value { - write!(dot, "\t{key} -> {dest}\n").unwrap_or_default(); + writeln!(dot, "\t{key} -> {dest}").unwrap_or_default(); } } diff --git a/apps/halidoscope/src-tauri/src/lib.rs b/apps/halidoscope/src-tauri/src/lib.rs index f704404f17c1..6552fb526d7d 100644 --- a/apps/halidoscope/src-tauri/src/lib.rs +++ b/apps/halidoscope/src-tauri/src/lib.rs @@ -42,7 +42,7 @@ pub fn run() { std::process::exit(0); } match matches.subcommand { - Some(subcommand) => halidoscope_cli(subcommand), + Some(subcommand) => halidoscope_cli(*subcommand), None => { tauri::WebviewWindowBuilder::from_config( app.handle(), diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index f4cd792d82a2..a62836447aae 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -537,7 +537,7 @@ impl Renderer for StoreFrequencyState { (NormalizationMode::AcrossFuncs, 0) => 0.0, (NormalizationMode::AcrossFuncs, global_max) => 255.0 / global_max as f64, (NormalizationMode::PerFunc, _) => { - let local_max = *&self.local_max_store_count; + let local_max = self.local_max_store_count; if local_max > 0 { 255.0 / local_max as f64 } else { @@ -996,9 +996,7 @@ impl ReuseDistanceState { pub fn new(trace: &Trace, func: &str) -> Option { let geom = trace.func_geometry(func)?; let n_cells = geom.width * geom.height * geom.channels; - let is_input = trace - .func_store_indices(func) - .map_or(true, |s| s.is_empty()); + let is_input = trace.func_store_indices(func).is_none_or(|s| s.is_empty()); let local_max_reuse_distance = trace.funcs.get(func).map(|s| s.max_reuse_distance)?; let global_max_reuse_distance = trace.funcs.values().map(|s| s.max_reuse_distance).max()?; @@ -1234,7 +1232,7 @@ impl ThreadState { let geom = trace.func_geometry(func)?; let thread_ids = trace .func_thread_ids(func) - .map(|ids| ids.iter().map(|&id| id as i32).collect::>()) + .map(|ids| ids.iter().copied().collect::>()) .unwrap_or_else(|| vec![0]); let n_threads = thread_ids.len(); diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 41a431f7b34f..46f00b80f63c 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -36,7 +36,7 @@ impl HalideType { // Obtain the number of bytes for a single scalar element (i.e., one SIMD lane) of a packet's // value. For sub-byte types, this rounds up to the nearest whole byte. pub fn elem_bytes(self) -> usize { - (self.bits as usize + 7) / 8 + (self.bits as usize).div_ceil(8) } // Obtain the number of bytes for the entire value of a packet. This is the product of the @@ -166,6 +166,7 @@ impl TracePacket { // ── Per-Func statistics ─────────────────────────────────────────────────────── #[derive(Debug, Clone)] +#[derive(Default)] pub struct FuncStats { pub name: String, pub min_coords: Vec, @@ -186,21 +187,6 @@ pub struct FuncStats { pub max_reuse_distance: u64, } -impl Default for FuncStats { - fn default() -> Self { - Self { - name: String::new(), - min_coords: vec![], - max_coords: vec![], - min_value: None, - max_value: None, - max_store_count: 0, - max_load_count: 0, - max_redundant_store_count: 0, - max_reuse_distance: 0, - } - } -} /// Full spatial layout of a Func: pixel dimensions plus the channel axis (logical dim 2). #[derive(Debug, Clone, Copy)] From a6f7fb845702c8ac4aec8ee61569dc338e87a1a5 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 4 Aug 2026 16:57:05 -0700 Subject: [PATCH 40/67] Reduce trace loading and parsing time 1.25x by combining stats computation into 2 (instead of 5) passes over store and load indices. Co-authored-by: Claude Opus 5 --- apps/halidoscope/src-tauri/src/commands.rs | 44 ++-- apps/halidoscope/src-tauri/src/render.rs | 9 +- apps/halidoscope/src-tauri/src/trace.rs | 240 +++++++++------------ 3 files changed, 111 insertions(+), 182 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index ca4f850de0cd..f4079e6a8b67 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -2,8 +2,9 @@ //! //! This module owns the types that cross the Tauri IPC boundary. -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, HashMap}; use std::sync::Mutex; +use std::time::Instant; use serde::{Deserialize, Serialize}; use tauri::ipc::Response; @@ -35,7 +36,6 @@ pub struct FuncMeta { pub width: u32, pub height: u32, pub channels: u32, - pub num_stores: u32, pub min_coords: Vec, pub max_coords: Vec, pub min_value: Option, @@ -71,12 +71,6 @@ pub struct TraceMeta { impl TraceMeta { pub fn from_trace(trace: &Trace) -> Self { - let mut global_max_store_count = 0u32; - let mut global_max_load_count = 0u32; - let mut global_max_redundant_store_count = 0u32; - let mut global_max_reuse_distance = 0u64; - let mut global_thread_ids: BTreeSet = BTreeSet::new(); - let funcs = trace .funcs .iter() @@ -86,26 +80,12 @@ impl TraceMeta { Some(g) => (g.width as u32, g.height as u32, g.channels as u32), None => (0, 0, 1), }; - let stores = trace.func_store_indices(name); - let num_stores = stores.map(<[usize]>::len).unwrap_or(0) as u32; - - global_max_store_count = stats.max_store_count.max(global_max_store_count); - global_max_load_count = stats.max_load_count.max(global_max_load_count); - global_max_redundant_store_count = stats - .max_redundant_store_count - .max(global_max_redundant_store_count); - global_max_reuse_distance = stats.max_reuse_distance.max(global_max_reuse_distance); - - if let Some(thread_ids) = trace.func_thread_ids(name) { - global_thread_ids.extend(thread_ids); - } FuncMeta { name: name.clone(), width, height, channels, - num_stores, min_coords: stats.min_coords.clone(), max_coords: stats.max_coords.clone(), min_value: stats.min_value, @@ -131,9 +111,6 @@ impl TraceMeta { .copied() .map(IndexRange::from_tuple) .collect(), - // A missing entry means `name` ran entirely serially (never inside a - // `BeginParallelTask`), not that it has no threads; default to the implicit - // serial thread `{0}` so `thread_ids` and `thread_count` agree. thread_count: trace .func_thread_ids(name) .map_or(1, |ids| ids.len() as u32), @@ -156,12 +133,13 @@ impl TraceMeta { total_packets: trace.packets.len() as u32, dag_edges, stats: StatsMeta { - global_max_store_count, - global_max_load_count, - global_max_redundant_store_count, - global_max_reuse_distance, - global_thread_ids: global_thread_ids - .into_iter() + global_max_store_count: trace.global_max_store_count, + global_max_load_count: trace.global_max_load_count, + global_max_redundant_store_count: trace.global_max_redundant_store_count, + global_max_reuse_distance: trace.global_max_reuse_distance, + global_thread_ids: trace + .global_thread_ids + .iter() .map(|id| id.to_string()) .collect(), }, @@ -225,10 +203,14 @@ pub async fn open_trace( state: State<'_, AppState>, ) -> Result { let (trace, meta) = tauri::async_runtime::spawn_blocking(move || { + let start = Instant::now(); let trace = Trace::load_from_file(&path, |pct| { let _ = app.emit("trace-load-progress", pct); })?; + let meta = TraceMeta::from_trace(&trace); + + eprintln!("open_trace took {:?}", start.elapsed()); Ok::<_, String>((trace, meta)) }) .await diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index a62836447aae..a28a445a78dd 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -1236,14 +1236,7 @@ impl ThreadState { .unwrap_or_else(|| vec![0]); let n_threads = thread_ids.len(); - let global_thread_ids: Vec = trace - .thread_ids_by_func - .values() - .flatten() - .copied() - .collect::>() - .into_iter() - .collect(); + let global_thread_ids: Vec = trace.global_thread_ids.iter().copied().collect(); // `-1` marks a pixel no store/load has touched yet. let thread_id_buffer = vec![-1; geom.width * geom.height]; diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 46f00b80f63c..8ca439def3ec 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -165,8 +165,7 @@ impl TracePacket { // ── Per-Func statistics ─────────────────────────────────────────────────────── -#[derive(Debug, Clone)] -#[derive(Default)] +#[derive(Debug, Clone, Default)] pub struct FuncStats { pub name: String, pub min_coords: Vec, @@ -187,7 +186,6 @@ pub struct FuncStats { pub max_reuse_distance: u64, } - /// Full spatial layout of a Func: pixel dimensions plus the channel axis (logical dim 2). #[derive(Debug, Clone, Copy)] pub struct FuncGeometry { @@ -203,7 +201,7 @@ pub struct FuncGeometry { pub max_reuse_distance: u64, } -// ── Complete trace ──────────────────────────────────────────────────────────── +// ── Complete trace ─────────────────────────────────────────────────────────────────────────────── // Note: We use BTreeMaps for deterministic iteration order here. We could consider switching to // HashMaps to get O(1) lookups if we find Func lookup starts to become a bottleneck. @@ -217,9 +215,14 @@ pub struct Trace { pub produce_ranges_by_func: BTreeMap>, pub consume_ranges_by_func: BTreeMap>, pub thread_ids_by_func: BTreeMap>, + pub global_max_store_count: u32, + pub global_max_load_count: u32, + pub global_max_redundant_store_count: u32, + pub global_max_reuse_distance: u64, + pub global_thread_ids: BTreeSet, } -// ── Binary parsing helpers ──────────────────────────────────────────────────── +// ── Binary parsing helpers ─────────────────────────────────────────────────────────────────────── // halide_trace_packet_t fixed header: 6 × 4 bytes = 24 bytes. // u32 size @ 0 @@ -397,7 +400,7 @@ fn parse_func_type_and_dim( } } -// ── Trace loading ───────────────────────────────────────────────────────────── +// ── Trace loading ──────────────────────────────────────────────────────────────────────────────── impl Trace { pub fn load_from_file(path: &str, on_progress: impl FnMut(u8)) -> Result { @@ -405,10 +408,6 @@ impl Trace { Self::load_from_bytes(&data, on_progress) } - /// Parses `data` into a `Trace`, invoking `on_progress` with the percentage (0-100) of bytes - /// consumed each time it crosses a new integer percentage point. Progress tracks bytes - /// consumed rather than packet count since the total packet count isn't known until parsing - /// completes (packets are variable-length). pub fn load_from_bytes(data: &[u8], mut on_progress: impl FnMut(u8)) -> Result { let total = data.len(); let mut pos = 0; @@ -424,6 +423,13 @@ impl Trace { let mut consume_ranges_by_func: BTreeMap> = BTreeMap::new(); let mut thread_ids_by_func: BTreeMap> = BTreeMap::new(); + // Trace-level maximum values. + let mut global_max_store_count = 0u32; + let mut global_max_load_count = 0u32; + let mut global_max_redundant_store_count = 0u32; + let mut global_max_reuse_distance = 0u64; + let mut global_thread_ids: BTreeSet = BTreeSet::new(); + // id -> (event, func_name, parent_id): needed for DAG inference after all packets are // parsed. let mut id_to_info: HashMap = HashMap::new(); @@ -714,64 +720,23 @@ impl Trace { .entry(pkt.func.clone()) .or_default() .insert(pkt.thread_id); - } - } - // Compute max per-pixel store/load counts for each Func using the index lists. We extract - // extents first (shared borrow) then write back (mut borrow) to keep the two borrows of - // `funcs` non-overlapping. - for (func_name, indices) in &store_indices_by_func { - let extents = funcs.get(func_name.as_str()).and_then(func_extents); - if let Some((w, h, min_x, min_y)) = extents { - let mut counts = vec![0u32; w * h]; - for &idx in indices { - let pkt = &packets[idx]; - for_each_lane_pixel( - pkt, - min_x, - min_y, - w, - h, - None, - |_lane, pixel_idx, _val_idx| { - counts[pixel_idx] += 1; - }, - ); - } - if let Some(stats) = funcs.get_mut(func_name.as_str()) { - stats.max_store_count = counts.iter().copied().max().unwrap_or(0); - } + // Insert the thread id into the global_thread_ids BTreeSet. + global_thread_ids.insert(pkt.thread_id); } } - for (func_name, indices) in &load_indices_by_func { - let extents = funcs.get(func_name.as_str()).and_then(func_extents); - if let Some((w, h, min_x, min_y)) = extents { - let mut counts = vec![0u32; w * h]; - for &idx in indices { - let pkt = &packets[idx]; - for_each_lane_pixel( - pkt, - min_x, - min_y, - w, - h, - None, - |_lane, pixel_idx, _val_idx| { - counts[pixel_idx] += 1; - }, - ); - } - if let Some(stats) = funcs.get_mut(func_name.as_str()) { - stats.max_load_count = counts.iter().copied().max().unwrap_or(0); - } - } - } - - // Compute max per-pixel redundant store counts: replay all stores for each Func, tracking - // the last value written to each (x, y, channel). A store is redundant when the incoming - // value bit-matches the previously stored value at that location and there have been no - // intervening loads from that location. + // Compute max per-pixel store count, load count, redundant store count, and reuse + // distance for each Func. All four statistics are derived from a single two-pointer + // merge of that Func's store/load indices in global packet order, so we compute them + // together in one walk rather than re-merging the same indices four separate times. + // (Redundant store count: a store is redundant when the incoming value bit-matches the + // previously stored value at that location and there have been no intervening loads from + // that location. Reuse distance: the packet-index gap between a store and the next load + // from the same (x, y, channel).) + // + // We extract extents/channels first (shared borrow) then write back (mut borrow) to keep + // the two borrows of `funcs` non-overlapping. for (func_name, store_indices) in &store_indices_by_func { let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { @@ -790,9 +755,14 @@ impl Trace { .map(Vec::as_slice) .unwrap_or(&[]); + let mut store_counts = vec![0u32; w * h]; + let mut load_counts = vec![0u32; w * h]; // None = no store has landed here yet; Some(bits) = last stored value as u64 bits. let mut last_values = vec![None::; w * h * channels]; let mut redundant_counts = vec![0u32; w * h]; + // usize::MAX = no store has landed at this (x, y, channel) yet. + let mut last_store_at = vec![usize::MAX; w * h * channels]; + let mut max_reuse_distances = vec![0u64; w * h]; let mut si = 0; let mut li = 0; @@ -811,25 +781,11 @@ impl Trace { min_y, w, h, - Some((min_c, channels)), - |lane, pixel_idx, val_idx| { - let Some(v) = pkt.decoded_value(lane) else { - return; - }; - let v_bits = v.to_bits(); - if let Some(prev_bits) = last_values[val_idx] { - if prev_bits == v_bits { - redundant_counts[pixel_idx] += 1; - } - } - last_values[val_idx] = Some(v_bits); + None, + |_lane, pixel_idx, _val_idx| { + store_counts[pixel_idx] += 1; }, ); - } else { - // If we observe a Load, reset the last_values slot for that location to None. - let global_idx = load_indices[li]; - li += 1; - let pkt = &packets[global_idx]; for_each_lane_pixel( pkt, min_x, @@ -837,61 +793,23 @@ impl Trace { w, h, Some((min_c, channels)), - |_lane, _pixel_idx, val_idx| { - last_values[val_idx] = None; + |lane, pixel_idx, val_idx| { + if let Some(v) = pkt.decoded_value(lane) { + let v_bits = v.to_bits(); + if let Some(prev_bits) = last_values[val_idx] { + if prev_bits == v_bits { + redundant_counts[pixel_idx] += 1; + } + } + last_values[val_idx] = Some(v_bits); + } + last_store_at[val_idx] = global_idx; }, ); - } - } - - if let Some(stats) = funcs.get_mut(func_name.as_str()) { - stats.max_redundant_store_count = - redundant_counts.iter().copied().max().unwrap_or(0); - } - } - } - - // Compute max per-pixel reuse distance for each Func. Two separate loops handle the two - // cases: - // - // 1. Intermediate Funcs (have stores): anchor = most recent store; distance measured to - // the next load from the same (x, y, channel). Events are two-pointer merged in - // global order. - // - // 2. Pipeline inputs (loads only, no stores): the first load at each pixel is a memcpy - // and is "free". Subsequent loads to the same pixel measure distance from that first - // load. Black = only one load ever (no reuse). - // - for (func_name, store_indices) in &store_indices_by_func { - let extents = funcs.get(func_name.as_str()).and_then(func_extents); - if let Some((w, h, min_x, min_y)) = extents { - let stats = funcs.get(func_name.as_str()).unwrap(); - let (channels, min_c) = if stats.min_coords.len() >= 3 { - ( - (stats.max_coords[2] - stats.min_coords[2]).max(1) as usize, - stats.min_coords[2], - ) - } else { - (1, 0) - }; - let load_indices = load_indices_by_func - .get(func_name.as_str()) - .map(Vec::as_slice) - .unwrap_or(&[]); - - // usize::MAX = no store has landed at this (x, y, channel) yet. - let mut last_store_at = vec![usize::MAX; w * h * channels]; - let mut max_reuse_distances = vec![0u64; w * h]; - let mut si = 0; - let mut li = 0; - - while si < store_indices.len() || li < load_indices.len() { - let next_is_store = si < store_indices.len() - && (li >= load_indices.len() || store_indices[si] < load_indices[li]); + } else { + let global_idx = load_indices[li]; + li += 1; - if next_is_store { - let global_idx = store_indices[si]; - si += 1; let pkt = &packets[global_idx]; for_each_lane_pixel( pkt, @@ -899,15 +817,11 @@ impl Trace { min_y, w, h, - Some((min_c, channels)), - |_lane, _pixel_idx, val_idx| { - last_store_at[val_idx] = global_idx; + None, + |_lane, pixel_idx, _val_idx| { + load_counts[pixel_idx] += 1; }, ); - } else { - let global_idx = load_indices[li]; - li += 1; - let pkt = &packets[global_idx]; for_each_lane_pixel( pkt, min_x, @@ -916,6 +830,11 @@ impl Trace { h, Some((min_c, channels)), |_lane, pixel_idx, val_idx| { + // A load resets redundancy tracking for this location: an + // intervening load means the next store, even if bit-identical, + // is not redundant. + last_values[val_idx] = None; + if last_store_at[val_idx] != usize::MAX { let dist = (global_idx - last_store_at[val_idx]) as u64; if dist > max_reuse_distances[pixel_idx] { @@ -928,17 +847,30 @@ impl Trace { } if let Some(stats) = funcs.get_mut(func_name.as_str()) { + stats.max_store_count = store_counts.iter().copied().max().unwrap_or(0); + stats.max_load_count = load_counts.iter().copied().max().unwrap_or(0); + stats.max_redundant_store_count = + redundant_counts.iter().copied().max().unwrap_or(0); stats.max_reuse_distance = max_reuse_distances.iter().copied().max().unwrap_or(0); + + global_max_store_count = global_max_store_count.max(stats.max_store_count); + global_max_load_count = global_max_load_count.max(stats.max_load_count); + global_max_redundant_store_count = + global_max_redundant_store_count.max(stats.max_redundant_store_count); + global_max_reuse_distance = + global_max_reuse_distance.max(stats.max_reuse_distance); } } } - // Pipeline inputs: Funcs with loads but no stores. The first load at each (x, y, channel) - // is free (analogous to a memcpy). Subsequent loads measure distance from that first load. + // Pipeline inputs: Funcs with loads but no stores. These aren't covered by the merged + // loop above, so compute their load count and reuse distance in one pass here. The first + // load at each (x, y, channel) is free (analogous to a memcpy); subsequent loads measure + // distance from that first load. for (func_name, load_indices) in &load_indices_by_func { if store_indices_by_func.contains_key(func_name.as_str()) { - continue; // handled by the store-anchor loop above + continue; // handled by the merged loop above } let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { @@ -952,12 +884,24 @@ impl Trace { (1, 0) }; + let mut load_counts = vec![0u32; w * h]; // usize::MAX = first load hasn't occurred at this (x, y, channel) yet. let mut first_load_at = vec![usize::MAX; w * h * channels]; let mut max_reuse_distances = vec![0u64; w * h]; for &global_idx in load_indices { let pkt = &packets[global_idx]; + for_each_lane_pixel( + pkt, + min_x, + min_y, + w, + h, + None, + |_lane, pixel_idx, _val_idx| { + load_counts[pixel_idx] += 1; + }, + ); for_each_lane_pixel( pkt, min_x, @@ -979,8 +923,13 @@ impl Trace { } if let Some(stats) = funcs.get_mut(func_name.as_str()) { + stats.max_load_count = load_counts.iter().copied().max().unwrap_or(0); stats.max_reuse_distance = max_reuse_distances.iter().copied().max().unwrap_or(0); + + global_max_load_count = global_max_load_count.max(stats.max_load_count); + global_max_reuse_distance = + global_max_reuse_distance.max(stats.max_reuse_distance); } } } @@ -997,6 +946,11 @@ impl Trace { produce_ranges_by_func, consume_ranges_by_func, thread_ids_by_func, + global_max_store_count, + global_max_load_count, + global_max_redundant_store_count, + global_max_reuse_distance, + global_thread_ids, }) } From ccf4613d28422c567eb555075d0d153a1337e3f5 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Wed, 5 Aug 2026 12:17:26 -0700 Subject: [PATCH 41/67] Adjust and document types for the frontend. Co-authored-by: Claude Opus 5 --- apps/halidoscope/src-tauri/src/commands.rs | 3 - apps/halidoscope/src/App.tsx | 3 +- .../src/components/canvas/Canvas.tsx | 2 +- .../src/components/canvas/FuncNode.tsx | 5 +- .../src/components/controls/ControlTabs.tsx | 2 +- .../src/components/controls/FuncsPanel.tsx | 2 +- .../components/controls/inf/InfControls.tsx | 9 +- .../components/controls/nan/NaNControls.tsx | 9 +- .../controls/playback/PlaybackRate.tsx | 3 +- .../src/components/views/profile/Treemap.tsx | 2 +- .../components/views/trace/TraceTimeline.tsx | 3 +- apps/halidoscope/src/hooks/profile.ts | 2 +- apps/halidoscope/src/hooks/trace.ts | 2 +- apps/halidoscope/src/state/inf.ts | 26 ---- apps/halidoscope/src/state/nan-inf.ts | 50 +++++++ apps/halidoscope/src/state/nan.ts | 26 ---- apps/halidoscope/src/state/playback.ts | 3 +- apps/halidoscope/src/types/index.ts | 80 ----------- apps/halidoscope/src/types/profile.ts | 127 ++++++++++++++++++ apps/halidoscope/src/types/trace.ts | 96 +++++++++++++ apps/halidoscope/src/utils/api.ts | 111 ++++++++++++++- apps/halidoscope/src/utils/constants.ts | 7 - apps/halidoscope/src/utils/graph.ts | 18 ++- apps/halidoscope/src/utils/liveness.ts | 35 ++++- 24 files changed, 460 insertions(+), 166 deletions(-) delete mode 100644 apps/halidoscope/src/state/inf.ts create mode 100644 apps/halidoscope/src/state/nan-inf.ts delete mode 100644 apps/halidoscope/src/state/nan.ts delete mode 100644 apps/halidoscope/src/types/index.ts create mode 100644 apps/halidoscope/src/types/profile.ts create mode 100644 apps/halidoscope/src/types/trace.ts delete mode 100644 apps/halidoscope/src/utils/constants.ts diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index f4079e6a8b67..8380581231bc 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -4,7 +4,6 @@ use std::collections::{BTreeMap, HashMap}; use std::sync::Mutex; -use std::time::Instant; use serde::{Deserialize, Serialize}; use tauri::ipc::Response; @@ -203,14 +202,12 @@ pub async fn open_trace( state: State<'_, AppState>, ) -> Result { let (trace, meta) = tauri::async_runtime::spawn_blocking(move || { - let start = Instant::now(); let trace = Trace::load_from_file(&path, |pct| { let _ = app.emit("trace-load-progress", pct); })?; let meta = TraceMeta::from_trace(&trace); - eprintln!("open_trace took {:?}", start.elapsed()); Ok::<_, String>((trace, meta)) }) .await diff --git a/apps/halidoscope/src/App.tsx b/apps/halidoscope/src/App.tsx index 5fdb54fa4f5b..b3f445da0564 100644 --- a/apps/halidoscope/src/App.tsx +++ b/apps/halidoscope/src/App.tsx @@ -12,7 +12,8 @@ import TraceLoading from "@/components/views/trace/TraceLoading"; import { ProfileContextProvider } from "@/hooks/profile"; import { TraceContextProvider } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; -import type { Profile as Pfile, FuncMeta, StatsMeta } from "@/types"; +import type { Profile as Pfile } from "@/types/profile"; +import type { FuncMeta, StatsMeta } from "@/types/trace"; import { openProfile, openTrace } from "@/utils/api"; import "./App.css"; diff --git a/apps/halidoscope/src/components/canvas/Canvas.tsx b/apps/halidoscope/src/components/canvas/Canvas.tsx index b5550983faa5..b9d24e9d523a 100644 --- a/apps/halidoscope/src/components/canvas/Canvas.tsx +++ b/apps/halidoscope/src/components/canvas/Canvas.tsx @@ -16,7 +16,7 @@ import Overlay from "@/components/canvas/Overlay"; import { funcAtom } from "@/state/func"; import { edgesAtom } from "@/state/graph"; import { livenessAtom } from "@/state/liveness"; -import { FuncMeta } from "@/types"; +import type { FuncMeta } from "@/types/trace"; import { buildEdges, buildNodes, getLayoutedElements } from "@/utils/graph"; const NODE_TYPES = { diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 637a9ee866e1..693e9df72fb4 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -17,14 +17,13 @@ import * as React from "react"; import HandleCircle from "@/components/canvas/HandleCircle"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; -import { infAtom } from "@/state/inf"; +import { infAtom, nanAtom } from "@/state/nan-inf"; import { livenessAtom } from "@/state/liveness"; import { packetAtom } from "@/state/packet"; -import { nanAtom } from "@/state/nan"; import { renderAtom } from "@/state/render"; import { tabularDataAtom } from "@/state/tabularData"; import { threadAtom } from "@/state/thread"; -import type { FuncMeta } from "@/types"; +import type { FuncMeta } from "@/types/trace"; import { renderGrayscale, renderRgb, diff --git a/apps/halidoscope/src/components/controls/ControlTabs.tsx b/apps/halidoscope/src/components/controls/ControlTabs.tsx index 932188370f39..61550d3a3181 100644 --- a/apps/halidoscope/src/components/controls/ControlTabs.tsx +++ b/apps/halidoscope/src/components/controls/ControlTabs.tsx @@ -4,7 +4,7 @@ import DebugPanel from "@/components/controls/DebugPanel"; import DisplayPanel from "@/components/controls/DisplayPanel"; import FuncsPanel from "@/components/controls/FuncsPanel"; import VisualizationPanel from "@/components/controls/VisualizationPanel"; -import { FuncMeta } from "@/types"; +import { FuncMeta } from "@/types/trace"; function ControlTabs({ funcs }: { funcs: Record }) { return ( diff --git a/apps/halidoscope/src/components/controls/FuncsPanel.tsx b/apps/halidoscope/src/components/controls/FuncsPanel.tsx index 664ef92701d8..70a051604d9a 100644 --- a/apps/halidoscope/src/components/controls/FuncsPanel.tsx +++ b/apps/halidoscope/src/components/controls/FuncsPanel.tsx @@ -1,8 +1,8 @@ import { useAtom } from "jotai"; import { Accordion } from "radix-ui"; -import type { FuncMeta } from "@/types"; import { funcAtom } from "@/state/func"; +import type { FuncMeta } from "@/types/trace"; interface FuncsPanelProps { funcs: Record; diff --git a/apps/halidoscope/src/components/controls/inf/InfControls.tsx b/apps/halidoscope/src/components/controls/inf/InfControls.tsx index 8650ba89063b..00200807a03d 100644 --- a/apps/halidoscope/src/components/controls/inf/InfControls.tsx +++ b/apps/halidoscope/src/components/controls/inf/InfControls.tsx @@ -5,9 +5,12 @@ import { Checkbox, Label, Select } from "radix-ui"; import ColorInput from "@/components/controls/color/ColorInput"; import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; import CheckIcon from "@/components/icons/CheckIcon"; -import { DEFAULT_INF_COLOR, infAtom } from "@/state/inf"; -import type { AnimationMode } from "@/types"; -import { ANIMATION_MODES } from "@/utils/constants"; +import { + DEFAULT_INF_COLOR, + infAtom, + ANIMATION_MODES, + type AnimationMode, +} from "@/state/nan-inf"; function InfControls() { const [inf, setInf] = useAtom(infAtom); diff --git a/apps/halidoscope/src/components/controls/nan/NaNControls.tsx b/apps/halidoscope/src/components/controls/nan/NaNControls.tsx index 17a40c7e5c1c..305ba36892fe 100644 --- a/apps/halidoscope/src/components/controls/nan/NaNControls.tsx +++ b/apps/halidoscope/src/components/controls/nan/NaNControls.tsx @@ -5,9 +5,12 @@ import { Checkbox, Label, Select } from "radix-ui"; import ColorInput from "@/components/controls/color/ColorInput"; import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; import CheckIcon from "@/components/icons/CheckIcon"; -import { DEFAULT_NAN_COLOR, nanAtom } from "@/state/nan"; -import type { AnimationMode } from "@/types"; -import { ANIMATION_MODES } from "@/utils/constants"; +import { + DEFAULT_NAN_COLOR, + nanAtom, + ANIMATION_MODES, + type AnimationMode, +} from "@/state/nan-inf"; function NaNControls() { const [nan, setNan] = useAtom(nanAtom); diff --git a/apps/halidoscope/src/components/controls/playback/PlaybackRate.tsx b/apps/halidoscope/src/components/controls/playback/PlaybackRate.tsx index 3a8243a3a9b9..21f03c64461f 100644 --- a/apps/halidoscope/src/components/controls/playback/PlaybackRate.tsx +++ b/apps/halidoscope/src/components/controls/playback/PlaybackRate.tsx @@ -1,8 +1,7 @@ import { useAtom } from "jotai"; import { Label, Slider } from "radix-ui"; -import { DEFAULT_PLAYBACK_RATE } from "@/utils/constants"; -import { playbackRateAtom } from "@/state/playback"; +import { playbackRateAtom, DEFAULT_PLAYBACK_RATE } from "@/state/playback"; const MIN_RATE = 100; const MAX_RATE = 20_000; diff --git a/apps/halidoscope/src/components/views/profile/Treemap.tsx b/apps/halidoscope/src/components/views/profile/Treemap.tsx index bab2400b8ea9..4a1cdedb7a8d 100644 --- a/apps/halidoscope/src/components/views/profile/Treemap.tsx +++ b/apps/halidoscope/src/components/views/profile/Treemap.tsx @@ -6,7 +6,7 @@ import * as React from "react"; import { useProfileContext } from "@/hooks/profile"; import { profileMetricAtom, type ProfileMetric } from "@/state/profile-metric"; -import type { Profile } from "@/types"; +import type { Profile } from "@/types/profile"; type TreemapNode = { name: string; diff --git a/apps/halidoscope/src/components/views/trace/TraceTimeline.tsx b/apps/halidoscope/src/components/views/trace/TraceTimeline.tsx index bd2e0134ee7c..2f9b8d2d561c 100644 --- a/apps/halidoscope/src/components/views/trace/TraceTimeline.tsx +++ b/apps/halidoscope/src/components/views/trace/TraceTimeline.tsx @@ -5,7 +5,8 @@ import * as React from "react"; import { packetAtom } from "@/state/packet"; import { playbackRateAtom } from "@/state/playback"; -import { SCRUB_DEBOUNCE_MS } from "@/utils/constants"; + +const SCRUB_DEBOUNCE_MS = 50; interface Props { packetCount: number; diff --git a/apps/halidoscope/src/hooks/profile.ts b/apps/halidoscope/src/hooks/profile.ts index 3c32c19ef035..c938c4afe674 100644 --- a/apps/halidoscope/src/hooks/profile.ts +++ b/apps/halidoscope/src/hooks/profile.ts @@ -1,6 +1,6 @@ import * as React from "react"; -import type { Profile } from "@/types"; +import type { Profile } from "@/types/profile"; const Profile = React.createContext({ pipelines: [], diff --git a/apps/halidoscope/src/hooks/trace.ts b/apps/halidoscope/src/hooks/trace.ts index f033ca76784c..f519f113845b 100644 --- a/apps/halidoscope/src/hooks/trace.ts +++ b/apps/halidoscope/src/hooks/trace.ts @@ -1,6 +1,6 @@ import * as React from "react"; -import { FuncMeta, StatsMeta } from "@/types"; +import { FuncMeta, StatsMeta } from "@/types/trace"; const TraceContext = React.createContext<{ funcs: Record; diff --git a/apps/halidoscope/src/state/inf.ts b/apps/halidoscope/src/state/inf.ts deleted file mode 100644 index a0cce1602a15..000000000000 --- a/apps/halidoscope/src/state/inf.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { atom } from "jotai"; - -import { AnimationMode } from "@/types"; - -export const DEFAULT_INF_COLOR = "#ffff00"; -export const DEFAULT_INF_ALPHA = 1; - -export const infAtom = atom<{ - active: boolean; - animationMode: AnimationMode; - color: { - r: number; - g: number; - b: number; - a: number; - }; -}>({ - active: false, - animationMode: "Blink", - color: { - r: 255, - g: 255, - b: 0, - a: DEFAULT_INF_ALPHA, - }, -}); diff --git a/apps/halidoscope/src/state/nan-inf.ts b/apps/halidoscope/src/state/nan-inf.ts new file mode 100644 index 000000000000..a315261eb7f6 --- /dev/null +++ b/apps/halidoscope/src/state/nan-inf.ts @@ -0,0 +1,50 @@ +import { atom } from "jotai"; + +export const ANIMATION_MODES = ["Blink", "Pulse", "None"] as const; +export type AnimationMode = (typeof ANIMATION_MODES)[number]; + +export const DEFAULT_NAN_COLOR = "#00ffff"; +export const DEFAULT_NAN_ALPHA = 1; + +export const nanAtom = atom<{ + active: boolean; + animationMode: AnimationMode; + color: { + r: number; + g: number; + b: number; + a: number; + }; +}>({ + active: false, + animationMode: "Blink", + color: { + r: 0, + g: 255, + b: 255, + a: DEFAULT_NAN_ALPHA, + }, +}); + +export const DEFAULT_INF_COLOR = "#ffff00"; +export const DEFAULT_INF_ALPHA = 1; + +export const infAtom = atom<{ + active: boolean; + animationMode: AnimationMode; + color: { + r: number; + g: number; + b: number; + a: number; + }; +}>({ + active: false, + animationMode: "Blink", + color: { + r: 255, + g: 255, + b: 0, + a: DEFAULT_INF_ALPHA, + }, +}); diff --git a/apps/halidoscope/src/state/nan.ts b/apps/halidoscope/src/state/nan.ts deleted file mode 100644 index b6b62696119f..000000000000 --- a/apps/halidoscope/src/state/nan.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { atom } from "jotai"; - -import { AnimationMode } from "@/types"; - -export const DEFAULT_NAN_COLOR = "#00ffff"; -export const DEFAULT_NAN_ALPHA = 1; - -export const nanAtom = atom<{ - active: boolean; - animationMode: AnimationMode; - color: { - r: number; - g: number; - b: number; - a: number; - }; -}>({ - active: false, - animationMode: "Blink", - color: { - r: 0, - g: 255, - b: 255, - a: DEFAULT_NAN_ALPHA, - }, -}); diff --git a/apps/halidoscope/src/state/playback.ts b/apps/halidoscope/src/state/playback.ts index 96d304bc049d..bf4879d92456 100644 --- a/apps/halidoscope/src/state/playback.ts +++ b/apps/halidoscope/src/state/playback.ts @@ -1,5 +1,6 @@ import { atom } from "jotai"; -import { DEFAULT_PLAYBACK_RATE } from "@/utils/constants"; +/** Default packets replayed in each tick during trace playback. */ +export const DEFAULT_PLAYBACK_RATE = 10000; export const playbackRateAtom = atom(DEFAULT_PLAYBACK_RATE); diff --git a/apps/halidoscope/src/types/index.ts b/apps/halidoscope/src/types/index.ts deleted file mode 100644 index 4e6d153f9034..000000000000 --- a/apps/halidoscope/src/types/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -export interface IndexRange { - start: number; - end: number; -} - -export interface FuncMeta extends Record { - name: string; - width: number; - height: number; - channels: number; - num_stores: number; - min_coords: number[]; - max_coords: number[]; - min_value: number | null; - max_value: number | null; - max_store_count: number; - max_load_count: number; - max_redundant_store_count: number; - max_reuse_distance: number; - buffer_liveness: IndexRange; - produce_ranges: IndexRange[]; - consume_ranges: IndexRange[]; - thread_count: number; - thread_ids: string[]; -} - -export interface StatsMeta { - global_max_store_count: number; - global_max_load_count: number; - global_max_redundant_store_count: number; - global_max_reuse_distance: number; - global_thread_ids: string[]; -} - -export interface TraceMeta { - funcs: FuncMeta[]; - total_packets: number; - dag_edges: Record; - stats: StatsMeta; -} - -export type NodeTypes = "funcNode"; -export type EdgeTypes = "funcEdge"; - -export type AnimationMode = "Blink" | "Pulse" | "None"; - -export interface ProfileFunc { - name: string; - parent: number; - canonical_id: number; - kind: number; - buffer_func_id: number; - time_ns: number; - memory_current: number; - memory_peak: number; - memory_total: number; - stack_peak: number; - active_threads_numerator: number; - active_threads_denominator: number; - num_allocs: number; -} - -export interface ProfilePipeline { - name: string; - runs: number; - billed_runs: number; - samples: number; - num_allocs: number; - time_ns: number; - memory_current: number; - memory_peak: number; - memory_total: number; - active_threads_numerator: number; - active_threads_denominator: number; - funcs: ProfileFunc[]; -} - -export interface Profile { - pipelines: ProfilePipeline[]; -} diff --git a/apps/halidoscope/src/types/profile.ts b/apps/halidoscope/src/types/profile.ts new file mode 100644 index 000000000000..98583e744b9b --- /dev/null +++ b/apps/halidoscope/src/types/profile.ts @@ -0,0 +1,127 @@ +/** + * Represents per-Func profiling stats for a single run of a pipeline, as + * captured by Halide's sampling profiler. + */ +export interface ProfileFunc { + /** The name of the Func. */ + name: string; + /** The id of the parent Func this one is `compute_at`. `-1` if the Func is + * `compute_root`. + */ + parent: number; + /** + * The id of this Func's canonical entry. + * + * @remarks + * + * A Func can appear in the funcs array more than once (e.g., an unscheduled + * Func with an update definition reached from multiple callers); + * `canonical_id` is the id of the first such appearance, the shared key for + * rolling instances back up to a single Func. + */ + canonical_id: number; + /** + * A tag identifying what this entry represents. + * + * @remarks + * + * `0` = an ordinary Func, + * `1` = profiler overhead bookkeeping, + * `2` = thread-idle bookkeeping, + * `3` = malloc, + * `4` = free, + * `5` = `copy_to_host`, + * `6` = `copy_to_device`, + * `7` = a `hoist_storage` allocation entry (carries the memory columns for + * the buffer's lifetime, while the time/compute columns belong to a separate + * production entry). + */ + kind: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + /** + * For `copy_to_host`/`copy_to_device` entries (see {@link ProfileFunc.kind}), + * the `canonical_id` of the Func whose buffer is being copied. `-1` otherwise. + */ + buffer_func_id: number; + /** The total time spent evaluating this Func, in nanoseconds. */ + time_ns: number; + /** The current memory allocation of this Func, in bytes. */ + memory_current: number; + /** The peak memory allocation of this Func, in bytes. */ + memory_peak: number; + /** The total memory allocation of this Func, in bytes. */ + memory_total: number; + /** The peak stack allocation across this Func's threads, in bytes. */ + stack_peak: number; + /** + * The numerator of the average number of thread pool worker threads active + * while computing this Func. Divide by {@link ProfileFunc.active_threads_denominator} + * to get the average. + */ + active_threads_numerator: number; + /** + * The denominator of the average number of thread pool worker threads active + * while computing this Func. + */ + active_threads_denominator: number; + /** The total number of times heap storage for this Func was allocated. */ + num_allocs: number; +} + +/** + * Represents profiling stats for a single run of a pipeline, as captured by + * Halide's sampling profiler. + */ +export interface ProfilePipeline { + /** The name of the pipeline. */ + name: string; + /** The number of times this pipeline has been run, ever, since the last reset. */ + runs: number; + /** + * The number of pipeline runs that produced at least one profiler sample. + * + * @remarks + * + * Runs that completed in less than one sampler tick contribute to + * {@link ProfilePipeline.runs} (and to the per-Func counters) but not to + * per-Func time accumulation, so this is the correct denominator for time + * averages. + */ + billed_runs: number; + /** The total number of samples taken inside this pipeline, across all its runs. */ + samples: number; + /** The total number of memory allocations made by Funcs in this pipeline,across all runs. */ + num_allocs: number; + /** + * The time billed to Funcs in this pipeline run by the sampling thread, + * in nanoseconds. + */ + time_ns: number; + /** The current memory allocation of Funcs in this pipeline run, in bytes. */ + memory_current: number; + /** The peak memory allocation of Funcs in this pipeline run, in bytes. */ + memory_peak: number; + /** The total memory allocation of Funcs in this pipeline run, in bytes. */ + memory_total: number; + /** + * The numerator of the average number of thread pool worker threads doing + * useful work while computing this pipeline run. Divide by + * {@link ProfilePipeline.active_threads_denominator} to get the average. + */ + active_threads_numerator: number; + /** + * The denominator of the average number of thread pool worker threads doing + * useful work while computing this pipeline run. + */ + active_threads_denominator: number; + /** Per-Func profiling stats for this pipeline run. */ + funcs: ProfileFunc[]; +} + +/** + * Represents the complete profiling payload for a pipeline run, as returned by + * the `Pipeline::halidoscope` capture or from a pre-recorded JSON file. + */ +export interface Profile { + /** The profiled pipelines captured in this profile snapshot. */ + pipelines: ProfilePipeline[]; +} diff --git a/apps/halidoscope/src/types/trace.ts b/apps/halidoscope/src/types/trace.ts new file mode 100644 index 000000000000..3bdb81cbc01a --- /dev/null +++ b/apps/halidoscope/src/types/trace.ts @@ -0,0 +1,96 @@ +/** + * Represents the extent of a liveness range for a Func. + */ +export interface LivenessRange { + /** The packet index where a given Func is live. */ + start: number; + /** The packet index where a given Func is no longer live. */ + end: number; +} + +/** + * Represents top-level metadata for a Func. + */ +export interface FuncMeta extends Record { + /** The name of the Func. */ + name: string; + /** The width of the Func's buffer. */ + width: number; + /** The height of the Func's buffer. */ + height: number; + /** The number of channels in the Func's buffer. */ + channels: number; + /** The minimum coordinate observed along each logical dimension. */ + min_coords: number[]; + /** The maximum (exclusive) coordinate observed along each logical dimension. */ + max_coords: number[]; + /** The minimum value observed across all loads/stores for this Func. */ + min_value: number | null; + /** The maximum value observed across all loads/stores for this Func. */ + max_value: number | null; + /** The maximum number of stores observed at any single coordinate for this Func. */ + max_store_count: number; + /** The maximum number of loads observed at any single coordinate for this Func. */ + max_load_count: number; + /** + * The maximum number of redundant stores observed at any single coordinate for this Func. + * + * @remarks + * + * A store is redundant when the incoming value bit-matches the previously stored + * value at that coordinate and there are no intervening loads from it. + */ + max_redundant_store_count: number; + /** + * The maximum store-to-load reuse distance observed across all coordinates for this Func. + * + * @remarks + * + * Measured as the difference in global packet indices between a store and the + * next load from the same coordinate. + */ + max_reuse_distance: number; + /** The packet index range over which this Func's buffer is live in memory. */ + buffer_liveness: LivenessRange; + /** The packet index ranges during which this Func is being produced. */ + produce_ranges: LivenessRange[]; + /** The packet index ranges during which this Func is being consumed. */ + consume_ranges: LivenessRange[]; + /** The number of distinct threads that executed this Func. */ + thread_count: number; + /** The IDs of the distinct threads that executed this Func. */ + thread_ids: string[]; +} + +/** + * Represents trace-wide statistics aggregated across all Funcs. + */ +export interface StatsMeta { + /** The maximum store count observed at any coordinate across all Funcs. */ + global_max_store_count: number; + /** The maximum load count observed at any coordinate across all Funcs. */ + global_max_load_count: number; + /** The maximum redundant store count observed at any coordinate across all Funcs. */ + global_max_redundant_store_count: number; + /** The maximum store-to-load reuse distance observed across all Funcs. */ + global_max_reuse_distance: number; + /** The IDs of every distinct thread observed across all Funcs. */ + global_thread_ids: string[]; +} + +/** + * Represents the complete metadata payload for a trace, as returned by the `open_trace` command. + */ +export interface TraceMeta { + /** Metadata for each Func in the trace. */ + funcs: FuncMeta[]; + /** The total number of packets in the trace. */ + total_packets: number; + /** + * A map from each Func's name to the names of the Funcs it consumes (i.e. its producers in the + * pipeline's DAG). + */ + dag_edges: Record; + /** Trace-wide statistics aggregated across all Funcs. */ + stats: StatsMeta; +} diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index b20292d79701..ee27cc3ba94c 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -2,26 +2,56 @@ import { invoke } from "@tauri-apps/api/core"; import type { NormalizationMode } from "@/state/render"; import type { ThreadOpMode } from "@/state/thread"; -import type { TraceMeta, Profile } from "@/types"; +import type { Profile } from "@/types/profile"; +import type { TraceMeta } from "@/types/trace"; +/** + * Parse a `.hltrace` file and return the metadata needed to set up canvases and + * the scrub timeline. Replaces any previously loaded trace. + * + * @param path The path to the `.hltrace` file to open. + * @returns The parsed {@link TraceMeta} for the trace. + */ export async function openTrace(path: string): Promise { return invoke("open_trace", { path }); } +/** The unpacked payload returned by a `render_*` command. */ export interface RenderFuncResponse { + /** The rendered RGBA8 tensor data for the Func's buffer. */ tensorData: Uint8ClampedArray; + /** The RGBA8 overlay marking coordinates where a NaN was observed. */ nanOverlayData: Uint8ClampedArray; + /** The RGBA8 overlay marking coordinates where an Inf was observed. */ infOverlayData: Uint8ClampedArray; + /** + * The per-coordinate tabular data backing histograms, or null if not + * requested. + */ tabularData: Uint32Array | null; } +/** Shared parameters accepted by every `render_*` command. */ export interface RenderFuncParams { + /** The name of the Func to render. */ func: string; + /** The global packet index to render up to. */ globalIndex: number; + /** Whether pixel values are normalized against all Funcs or just this one. */ normalizationMode: NormalizationMode; + /** The width of the Func's buffer. */ width: number; + /** The height of the Func's buffer. */ height: number; + /** + * Whether to compute and return per-coordinate tabular data alongside the + * rendered pixels. + */ includeTabularData: boolean; + /** + * The overlay color to apply at coordinates where a NaN was observed, + * if `active`. + */ includeNan: { active: boolean; r: number; @@ -29,6 +59,10 @@ export interface RenderFuncParams { b: number; a: number; }; + /** + * The overlay color to apply at coordinates where an Inf was observed, + * if `active`. + */ includeInf: { active: boolean; r: number; @@ -87,6 +121,13 @@ function splitRenderBuffer({ }; } +/** + * Render a Func as a grayscale image at a given packet index. + * + * @param params The {@link RenderFuncParams} describing what to render. + * @returns The {@link RenderFuncResponse} split out from the backend's raw + * buffer. + */ export async function renderGrayscale({ func, globalIndex, @@ -114,6 +155,14 @@ export async function renderGrayscale({ }); } +/** + * Render a Func as an RGB image at a given packet index. Channels 0/1/2 map to + * R/G/B. + * + * @param params The {@link RenderFuncParams} describing what to render. + * @returns The {@link RenderFuncResponse} split out from the backend's raw + * buffer. + */ export async function renderRgb({ func, globalIndex, @@ -141,6 +190,13 @@ export async function renderRgb({ }); } +/** + * Render a heatmap of store counts for a Func at a given packet index. + * + * @param params The {@link RenderFuncParams} describing what to render. + * @returns The {@link RenderFuncResponse} split out from the backend's raw + * buffer. + */ export async function renderStoreFrequency({ func, globalIndex, @@ -168,6 +224,13 @@ export async function renderStoreFrequency({ }); } +/** + * Render a heatmap of load counts for a Func at a given packet index. + * + * @param params The {@link RenderFuncParams} describing what to render. + * @returns The {@link RenderFuncResponse} split out from the backend's raw + * buffer. + */ export async function renderLoadFrequency({ func, globalIndex, @@ -195,6 +258,19 @@ export async function renderLoadFrequency({ }); } +/** + * Render a heatmap of redundant store counts for a Func at a given packet + * index. + * + * @remarks + * + * A store is redundant when it writes the same value to a location that already + * holds that value _and_ no intervening load has read that value. + * + * @param params The {@link RenderFuncParams} describing what to render. + * @returns The {@link RenderFuncResponse} split out from the backend's raw + * buffer. + */ export async function renderRedundantStores({ func, globalIndex, @@ -222,6 +298,20 @@ export async function renderRedundantStores({ }); } +/** + * Render a heatmap of maximum store-to-load reuse distances for a Func at a + * given packet index. + * + * @remarks + * + * Reuse distance is the number of packets elapsed between a store and the next + * load from the same (x, y, channel). In the case of input buffers, it is the + * distance from the first load to the last load from that buffer. + * + * @param params The {@link RenderFuncParams} describing what to render. + * @returns The {@link RenderFuncResponse} split out from the backend's raw + * buffer. + */ export async function renderReuseDistance({ func, globalIndex, @@ -249,11 +339,24 @@ export async function renderReuseDistance({ }); } +/** Parameters accepted by the `render_thread` command. */ export interface RenderThreadFuncParams extends RenderFuncParams { + /** + * Whether to render the store or load operations attributed to `threadId`. + */ threadOpMode: ThreadOpMode; + /** The ID of the thread to render coverage for. */ threadId: string; } +/** + * Render a heatmap of the coordinates stored to or loaded from by a single + * thread for a Func at a given packet index. + * + * @param params The {@link RenderThreadFuncParams} describing what to render. + * @returns The {@link RenderFuncResponse} split out from the backend's raw + * buffer. + */ export async function renderThread({ func, globalIndex, @@ -284,6 +387,12 @@ export async function renderThread({ }); } +/** + * Parse a Halide profiler output file and return its contents. + * + * @param path The path to the profiler output file to open. + * @returns The parsed {@link Profile}. + */ export async function openProfile(path: string): Promise { return invoke("open_profile", { path }); } diff --git a/apps/halidoscope/src/utils/constants.ts b/apps/halidoscope/src/utils/constants.ts deleted file mode 100644 index 9d00a2ef6c27..000000000000 --- a/apps/halidoscope/src/utils/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AnimationMode } from "@/types"; - -export const DEFAULT_PLAYBACK_RATE = 10000; -/** Debounce window in ms before a settled scrub position is rendered. */ -export const SCRUB_DEBOUNCE_MS = 50; - -export const ANIMATION_MODES: AnimationMode[] = ["Blink", "Pulse", "None"]; diff --git a/apps/halidoscope/src/utils/graph.ts b/apps/halidoscope/src/utils/graph.ts index 0d8554b39d89..e60bf280d8d4 100644 --- a/apps/halidoscope/src/utils/graph.ts +++ b/apps/halidoscope/src/utils/graph.ts @@ -1,10 +1,14 @@ import Dagre from "@dagrejs/dagre"; import type { Node, Edge } from "@xyflow/react"; -import { EdgeTypes, FuncMeta, NodeTypes } from "@/types"; +import type { FuncMeta } from "@/types/trace"; + +/** Represents the types of permitted nodes for the @xyflow/react canvas. */ +type NodeTypes = "funcNode"; /** - * Build xyflow nodes from the backend's funcs payload, which maps Halide func + * Build xyflow nodes from the backend's funcs payload. + * * @param funcs The funcs payload from the backend. * @param type The node type to assign to each node. * @returns An array of nodes formatted for use with @xyflow/react. @@ -30,6 +34,9 @@ export function buildNodes( }); } +/** Represents the types of permitted edges for the @xyflow/react canvas. */ +type EdgeTypes = "funcEdge"; + /** * Build xyflow edges from the backend's dag_edges payload, which maps Halide * consumers to their producers. @@ -63,6 +70,13 @@ export function buildEdges( return edges; } +/** + * Lay out the Halide pipeline as a DAG with Dagre and return nodes and edges + * augmented with positional information. + * + * @param nodes The set of Halide funcs in the pipeline. + * @param edges The edges representing dataflow between Halide funcs. + */ export function getLayoutedElements( nodes: Node[], edges: Edge[], diff --git a/apps/halidoscope/src/utils/liveness.ts b/apps/halidoscope/src/utils/liveness.ts index 687a0ebc251c..f3a664894881 100644 --- a/apps/halidoscope/src/utils/liveness.ts +++ b/apps/halidoscope/src/utils/liveness.ts @@ -1,5 +1,13 @@ -import type { FuncMeta } from "@/types"; +import type { FuncMeta } from "@/types/trace"; +/** + * Determine whether a Func's buffer is live in memory at a given point in the + * åtrace. + * + * @param func The Func metadata to check. + * @param globalIndex The global packet index to check liveness at. + * @returns Whether the Func's buffer is live at `globalIndex`. + */ export function isFuncBufferLive(func: FuncMeta, globalIndex: number) { return ( func.buffer_liveness.start <= globalIndex && @@ -7,18 +15,43 @@ export function isFuncBufferLive(func: FuncMeta, globalIndex: number) { ); } +/** + * Determine whether a Func is being consumed at a given point in the trace. + * + * @param func The Func metadata to check. + * @param globalIndex The global packet index to check against. + * @returns Whether `globalIndex` falls within one of the Func's consume ranges. + */ export function isFuncConsuming(func: FuncMeta, globalIndex: number) { return func.consume_ranges.some( (range) => range.start <= globalIndex && globalIndex <= range.end, ); } +/** + * Determine whether a Func is being produced at a given point in the trace. + * + * @param func The Func metadata to check. + * @param globalIndex The global packet index to check against. + * @returns Whether `globalIndex` falls within one of the Func's produce ranges. + */ export function isFuncProducing(func: FuncMeta, globalIndex: number) { return func.produce_ranges.some( (range) => range.start <= globalIndex && globalIndex <= range.end, ); } +/** + * Determine whether the dataflow edge between a producer and consumer Func is + * live at a given point in the trace. + * + * @param funcs A map from Func name to its metadata. + * @param source The name of the producer Func. + * @param target The name of the consumer Func. + * @param globalIndex The global packet index to check against. + * @returns Whether `source` is producing and `target` is consuming at + * `globalIndex`. + */ export function isEdgeLive( funcs: Record, source: string, From e503a39ed6d2cd99732b86f18cb6d1403d6f680c Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Wed, 5 Aug 2026 15:06:52 -0700 Subject: [PATCH 42/67] Add HalidoscopeOptions to allow callers of Pipeline::halidoscope to specify a path to the halidoscope binary on disk. --- .../src/halide/halide_/PyPipeline.cpp | 25 +++++++----- src/Pipeline.cpp | 38 ++++++++++++++----- src/Pipeline.h | 19 +++++++--- 3 files changed, 59 insertions(+), 23 deletions(-) diff --git a/python_bindings/src/halide/halide_/PyPipeline.cpp b/python_bindings/src/halide/halide_/PyPipeline.cpp index a76ea104fb2c..70589163f6e0 100644 --- a/python_bindings/src/halide/halide_/PyPipeline.cpp +++ b/python_bindings/src/halide/halide_/PyPipeline.cpp @@ -64,6 +64,13 @@ void define_pipeline(py::module &m) { return ""; }); + py::class_(m, "HalidoscopeOptions") + .def(py::init<>()) + .def_readwrite("halidoscope_path", &HalidoscopeOptions::halidoscope_path) + .def("__repr__", [](const HalidoscopeOptions &o) -> std::string { + return ""; + }); + auto pipeline_class = py::class_(m, "Pipeline") .def(py::init<>()) @@ -231,30 +238,30 @@ void define_pipeline(py::module &m) { // Blocks until the Halidoscope window is closed, so the GIL must be // released for the duration of the call, same as realize() above. .def("halidoscope", // - [](Pipeline &p, Buffer<> buffer, const Target &target) -> void { + [](Pipeline &p, Buffer<> buffer, const HalidoscopeOptions &options, const Target &target) -> void { py::gil_scoped_release release; - p.halidoscope(Realization(std::move(buffer)), target); // + p.halidoscope(Realization(std::move(buffer)), options, target); // }, - py::arg("dst"), py::arg("target") = Target()) + py::arg("dst"), py::arg("options") = HalidoscopeOptions(), py::arg("target") = Target()) // See the comment on the corresponding realize() overload above: this // overload must be declared before the list-of-sizes one, so that an // empty list [] is resolved as list-of-sizes (a 0-dimensional Buffer) // rather than as an ambiguous empty list-of-buffers. .def("halidoscope", // - [](Pipeline &p, std::vector sizes, const Target &target) -> void { + [](Pipeline &p, std::vector sizes, const HalidoscopeOptions &options, const Target &target) -> void { py::gil_scoped_release release; - p.halidoscope(std::move(sizes), target); // + p.halidoscope(std::move(sizes), options, target); // }, - py::arg("sizes") = std::vector{}, py::arg("target") = Target()) + py::arg("sizes") = std::vector{}, py::arg("options") = HalidoscopeOptions(), py::arg("target") = Target()) // This will actually allow a list-of-buffers as well as a tuple-of-buffers, but that's OK. .def("halidoscope", // - [](Pipeline &p, std::vector> buffers, const Target &target) -> void { + [](Pipeline &p, std::vector> buffers, const HalidoscopeOptions &options, const Target &target) -> void { py::gil_scoped_release release; - p.halidoscope(Realization(std::move(buffers)), target); // + p.halidoscope(Realization(std::move(buffers)), options, target); // }, - py::arg("dst"), py::arg("target") = Target()) + py::arg("dst"), py::arg("options") = HalidoscopeOptions(), py::arg("target") = Target()) .def("infer_input_bounds", // [](Pipeline &p, const py::object &dst, const Target &target) -> void { diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index cfb3f18b3275..9b5c5d948bf6 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -1076,9 +1076,23 @@ Pipeline::RealizationArg halidoscope_clone_output(const Pipeline::RealizationArg } // namespace void Pipeline::halidoscope_impl(const std::function &do_realize, + const HalidoscopeOptions &options, const Target &target_arg) { user_assert(defined()) << "Pipeline is undefined\n"; + // Fail fast if halidoscope_path looks like an explicit path (as opposed + // to a bare name meant to be resolved via $PATH, e.g. the default + // "halidoscope") and nothing exists there -- no need to burn two full + // pipeline realizations before discovering the binary is missing. A bare + // name can't be checked this way (file_exists() just wraps access(), + // which won't search $PATH), so that case is instead caught below, after + // actually trying to launch it. + if (options.halidoscope_path.find('/') != std::string::npos) { + user_assert(file_exists(options.halidoscope_path)) + << "halidoscope: no file found at HalidoscopeOptions::halidoscope_path='" + << options.halidoscope_path << "'.\n"; + } + // Pipeline::compile_jit() discards the *entire* target (feature bits // included) and replaces it with get_jit_target_from_environment() // whenever has_unknowns() is true -- so an unresolved target_arg (e.g. @@ -1137,26 +1151,32 @@ void Pipeline::halidoscope_impl(const std::function sizes, const Target &target) { - halidoscope_impl([&sizes](Pipeline &p, const Target &t) { p.realize(sizes, t); }, target); +void Pipeline::halidoscope(std::vector sizes, HalidoscopeOptions options, const Target &target) { + halidoscope_impl([&sizes](Pipeline &p, const Target &t) { p.realize(sizes, t); }, options, target); } -void Pipeline::halidoscope(RealizationArg output, const Target &target) { +void Pipeline::halidoscope(RealizationArg output, HalidoscopeOptions options, const Target &target) { halidoscope_impl([&output](Pipeline &p, const Target &t) { p.realize(halidoscope_clone_output(output), t); }, - target); + options, target); } // Make a vector of void *'s to pass to the jit call using the diff --git a/src/Pipeline.h b/src/Pipeline.h index a1932e554a19..29fe7a03a41d 100644 --- a/src/Pipeline.h +++ b/src/Pipeline.h @@ -114,6 +114,14 @@ struct AutoSchedulerResults { std::vector featurization; // The featurization of the pipeline (if any) }; +/** Options controlling how Pipeline::halidoscope() locates and launches the + * Halidoscope GUI binary. */ +struct HalidoscopeOptions { + /** Path to the halidoscope executable, or just its name if it's on + * $PATH. Defaults to looking it up on $PATH. */ + std::string halidoscope_path = "halidoscope"; +}; + class Pipeline; using AutoSchedulerFn = std::function; @@ -519,9 +527,9 @@ class Pipeline { * profiler enabled), write the resulting trace and profile artifacts to * a temporary directory, and open them in the Halidoscope GUI * (https://github.com/halide/Halide, apps/halidoscope). The - * `halidoscope` executable is looked up on $PATH, unless the - * HALIDOSCOPE_PATH environment variable is set, in which case that path - * is used instead. + * `halidoscope` executable is looked up on $PATH by default; pass a + * HalidoscopeOptions with halidoscope_path set to override + * that. * * This performs two additional realizations of the pipeline purely for * the sake of instrumentation -- it does not realize the "real" output @@ -532,14 +540,15 @@ class Pipeline { * Not reentrant/thread-safe; do not call this concurrently with itself * or with another Halide JIT realization in the same process. */ // @{ - void halidoscope(std::vector sizes = {}, const Target &target = Target()); - void halidoscope(RealizationArg output, const Target &target = Target()); + void halidoscope(std::vector sizes = {}, HalidoscopeOptions options = HalidoscopeOptions(), const Target &target = Target()); + void halidoscope(RealizationArg output, HalidoscopeOptions options = HalidoscopeOptions(), const Target &target = Target()); // @} private: std::string generate_function_name() const; void halidoscope_impl(const std::function &do_realize, + const HalidoscopeOptions &options, const Target &target_arg); }; From 44d4ca44b0d33501d06dd2b1e0631e69c25aa0ba Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 6 Aug 2026 10:28:06 -0700 Subject: [PATCH 43/67] Allow callers of Pipeline::halidoscope to specify a non-volatile directory for persisting generated Halide trace and profile. --- src/Pipeline.cpp | 12 ++++++++---- src/Pipeline.h | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index 9b5c5d948bf6..0470aa789aa4 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -1105,7 +1105,7 @@ void Pipeline::halidoscope_impl(const std::function data; serialize_pipeline(*this, data, external_params); - std::string dir = dir_make_temp(); + std::string dir = options.halidoscope_output_dir ? *options.halidoscope_output_dir : dir_make_temp(); std::string trace_path = dir + "/trace.hltrace"; std::string profile_path = dir + "/profile.json"; @@ -1154,9 +1154,13 @@ void Pipeline::halidoscope_impl(const std::function halidoscope_output_dir = std::nullopt; }; class Pipeline; From 881ce00880e4d3d80d3febded0e4ae5baefb8139 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 6 Aug 2026 11:34:52 -0700 Subject: [PATCH 44/67] Fix missing end bin in histogram rendering and shift all render mode GUI controls into RenderModeParameters component. --- .../controls/VisualizationPanel.tsx | 126 ++++---- .../controls/bar-chart/BarChartParameters.tsx | 162 ---------- .../{bar-chart => charts}/BarChart.tsx | 0 .../{histogram => charts}/Histogram.tsx | 23 +- .../histogram/HistogramParameters.tsx | 163 ---------- .../controls/render/RenderModeParameters.tsx | 290 ++++++++++++++++++ 6 files changed, 355 insertions(+), 409 deletions(-) delete mode 100644 apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx rename apps/halidoscope/src/components/controls/{bar-chart => charts}/BarChart.tsx (100%) rename apps/halidoscope/src/components/controls/{histogram => charts}/Histogram.tsx (80%) delete mode 100644 apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx create mode 100644 apps/halidoscope/src/components/controls/render/RenderModeParameters.tsx diff --git a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx index 39560e39a162..9feef9de7a31 100644 --- a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -4,11 +4,10 @@ import { Separator } from "radix-ui"; import * as React from "react"; import ControlSection from "@/components/controls/ControlSection"; -import BarChart from "@/components/controls/bar-chart/BarChart"; -import BarChartParameters from "@/components/controls/bar-chart/BarChartParameters"; -import Histogram from "@/components/controls/histogram/Histogram"; -import HistogramParameters from "@/components/controls/histogram/HistogramParameters"; +import BarChart from "@/components/controls/charts/BarChart"; +import Histogram from "@/components/controls/charts/Histogram"; import RenderMode from "@/components/controls/render/RenderMode"; +import RenderModeParameters from "@/components/controls/render/RenderModeParameters"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; import { type RenderMode as RM, renderAtom } from "@/state/render"; @@ -38,17 +37,6 @@ const METRIC_PALETTE = [ "#D64000", ]; -interface HistogramData { - type: "Histogram"; - data: { x1: number; x2: number; y0: number; y1: number; color: string }[]; - domain: [number, number]; - range: string[]; -} - -// Standard subtractive-light combinations used to render RGB's per-channel histograms as -// stacked, flat-colored bands (e.g. overlapping red and green bars become one yellow band) — -// the same result `mix-blend-mode: screen` produces, but precomputed so it doesn't depend on -// (and get washed out by) whatever's behind the chart. const PURE_CHANNEL_COLORS: Record<"r" | "g" | "b", string> = { r: "#ff0000", g: "#00ff00", @@ -63,6 +51,13 @@ const PAIR_CHANNEL_COLORS: Record = { const TRIPLE_CHANNEL_COLOR = "#ffffff"; +interface HistogramData { + type: "Histogram"; + data: { x1: number; x2: number; y0: number; y1: number; color: string }[]; + domain: [number, number]; + renderLegend: boolean; +} + interface BarChartData { type: "Bar Chart"; data: { x: string; y: number }[]; @@ -70,14 +65,7 @@ interface BarChartData { range: string[]; } -interface NoChartData { - type: "No Chart"; - data: number[]; - domain: [number, number]; - range: string[]; -} - -type ChartData = HistogramData | BarChartData | NoChartData; +type ChartData = HistogramData | BarChartData; function VisualizationPanel() { const { funcs, stats } = useTraceContext(); @@ -96,9 +84,6 @@ function VisualizationPanel() { const extent = domain[1] - domain[0]; const step = domain.every(Number.isInteger) && extent <= 64 ? 1 : extent / buckets; - // Colors are resolved to literal values here (rather than left to Plot's shared `color` - // scale) so that this histogram can be composed alongside others (e.g. RGB's stacked - // per-channel bands) without needing a single shared domain-to-color mapping. const colorScale = d3 .scaleLinear() .domain( @@ -122,8 +107,8 @@ function VisualizationPanel() { color: colorScale(x1), }; }), - domain, - range, + domain: [domain[0], domain[1] + step], + renderLegend: true, }; }, [], @@ -162,6 +147,7 @@ function VisualizationPanel() { if (lo[1] > 0) { data.push({ x1, x2, y0: 0, y1: lo[1], color: TRIPLE_CHANNEL_COLOR }); } + if (mid[1] > lo[1]) { const pairKey = [mid[0], hi[0]].sort().join(""); data.push({ @@ -172,6 +158,7 @@ function VisualizationPanel() { color: PAIR_CHANNEL_COLORS[pairKey], }); } + if (hi[1] > mid[1]) { data.push({ x1, @@ -187,13 +174,13 @@ function VisualizationPanel() { type: "Histogram", data, domain, - range: [], + renderLegend: true, }; }, [], ); - const { type, data, domain, range } = React.useMemo((): ChartData => { + const chartData = React.useMemo((): ChartData => { switch (render.renderMode) { case "Grayscale": { const min = funcs[activeFunc].min_value ?? 0; @@ -304,9 +291,6 @@ function VisualizationPanel() { }, []), }; } - default: { - return { type: "No Chart", data: [], domain: [-1, -1], range: [] }; - } } }, [ render, @@ -321,60 +305,54 @@ function VisualizationPanel() { ]); const renderChart = React.useCallback(() => { - switch (type) { + switch (chartData.type) { case "Histogram": return ( - <> - - - - - - - + ); case "Bar Chart": return ( - <> - - - - - x === thread.id || thread.id === NO_THREAD_INFO_SENTINEL_ID - } - /> - - - + + x === thread.id || thread.id === NO_THREAD_INFO_SENTINEL_ID + } + /> ); - case "No Chart": - return ; } - }, [type, data, domain, range, scale, render.renderMode, thread]); + }, [chartData, scale, render.renderMode, thread]); return (
- {renderChart()} + + + + {renderChart()} + +
); } diff --git a/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx b/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx deleted file mode 100644 index e3839d219906..000000000000 --- a/apps/halidoscope/src/components/controls/bar-chart/BarChartParameters.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Label, Select } from "radix-ui"; -import { useAtom } from "jotai"; - -import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; -import { useTraceContext } from "@/hooks/trace"; -import { funcAtom } from "@/state/func"; -import { threadAtom, NO_THREAD_INFO_SENTINEL_ID } from "@/state/thread"; - -function BarChartParameters() { - const { funcs } = useTraceContext(); - const [activeFunc, setActiveFunc] = useAtom(funcAtom); - const [thread, setThread] = useAtom(threadAtom); - - return ( -
-
- - Selected Func - - { - setActiveFunc(value); - }} - > - - - - - - - - - - - {Object.keys(funcs).map((func) => ( - - - {func} - - - ))} - - - -
-
-
- - Operation - - { - setThread({ ...thread, op: value as "Load" | "Store" }); - }} - > - - - - - - - - - - - - Store - - - Load - - - - -
-
- - Filter by Thread - - { - setThread({ - ...thread, - id: value === "None" ? NO_THREAD_INFO_SENTINEL_ID : value, - }); - }} - > - - - - - - - - - - - - None - - {funcs[activeFunc].thread_ids - .filter((threadId) => threadId !== "0") - .map((threadId) => ( - - {threadId} - - ))} - - - -
-
-
- ); -} - -export default BarChartParameters; diff --git a/apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx b/apps/halidoscope/src/components/controls/charts/BarChart.tsx similarity index 100% rename from apps/halidoscope/src/components/controls/bar-chart/BarChart.tsx rename to apps/halidoscope/src/components/controls/charts/BarChart.tsx diff --git a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx b/apps/halidoscope/src/components/controls/charts/Histogram.tsx similarity index 80% rename from apps/halidoscope/src/components/controls/histogram/Histogram.tsx rename to apps/halidoscope/src/components/controls/charts/Histogram.tsx index 034875f0593b..e91eb28c2a2c 100644 --- a/apps/halidoscope/src/components/controls/histogram/Histogram.tsx +++ b/apps/halidoscope/src/components/controls/charts/Histogram.tsx @@ -7,15 +7,23 @@ import type { Scale } from "@/state/tabularData"; interface HistogramProps { data: { x1: number; x2: number; y0: number; y1: number; color: string }[]; domain: [number, number]; - range: string[]; scale: Scale; labels: { x: string; y: string; }; + renderLegend: boolean; + interval?: number; } -function Histogram({ data, domain, scale, range, labels }: HistogramProps) { +function Histogram({ + data, + domain, + scale, + labels, + renderLegend, + interval, +}: HistogramProps) { const ref = React.useRef(null); React.useEffect(() => { @@ -44,11 +52,9 @@ function Histogram({ data, domain, scale, range, labels }: HistogramProps) { tickPadding: 24, tickSize: 0, type: scale, + interval, }, marks: [ - // Bars carry a literal, precomputed `color` (rather than relying on Plot's shared - // `color` scale), since RGB's stacked per-channel bands need per-band colors that a - // single domain-to-color mapping can't express. Plot.rect(data, { x1: "x1", x2: "x2", @@ -56,10 +62,7 @@ function Histogram({ data, domain, scale, range, labels }: HistogramProps) { y2: "y1", fill: "color", }), - // RGB's stacked bands don't have one representative color per bucket, so the axis - // color strip below is only meaningful (and only supplied via `range`) for the - // single-series histograms. - ...(range.length > 0 + ...(renderLegend ? [ Plot.ruleY(data, { stroke: "color", @@ -79,7 +82,7 @@ function Histogram({ data, domain, scale, range, labels }: HistogramProps) { return () => { plot.remove(); }; - }, [data, domain, labels, range, scale]); + }, [data, domain, labels, scale, renderLegend, interval]); return data.every((d) => d.y1 === d.y0) ? (
diff --git a/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx b/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx deleted file mode 100644 index d6f3450aa224..000000000000 --- a/apps/halidoscope/src/components/controls/histogram/HistogramParameters.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { Label, Select } from "radix-ui"; -import { useAtom } from "jotai"; - -import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; -import { useTraceContext } from "@/hooks/trace"; -import { funcAtom } from "@/state/func"; -import { tabularDataAtom, type Scale } from "@/state/tabularData"; -import { type NormalizationMode, renderAtom } from "@/state/render"; - -function HistogramParameters() { - const { funcs } = useTraceContext(); - const [activeFunc, setActiveFunc] = useAtom(funcAtom); - const [tabularData, setTabularData] = useAtom(tabularDataAtom); - const [render, setRender] = useAtom(renderAtom); - - return ( -
-
- - Selected Func - - { - setActiveFunc(value); - }} - > - - - - - - - - - - - {Object.keys(funcs).map((func) => ( - - - {func} - - - ))} - - - -
- {render.renderMode !== "Grayscale" && render.renderMode !== "RGB" ? ( -
-
- - Scale - - - setTabularData({ ...tabularData, scale: value as Scale }) - } - > - - - - - - - - - - Linear - - - Log - - - - -
-
- - Normalize Display - - - setRender({ - ...render, - normalizationMode: value as NormalizationMode, - }) - } - > - - - - - - - - - - Across Funcs - - - Per Func - - - - -
-
- ) : null} -
- ); -} - -export default HistogramParameters; diff --git a/apps/halidoscope/src/components/controls/render/RenderModeParameters.tsx b/apps/halidoscope/src/components/controls/render/RenderModeParameters.tsx new file mode 100644 index 000000000000..3500eeeb8bbc --- /dev/null +++ b/apps/halidoscope/src/components/controls/render/RenderModeParameters.tsx @@ -0,0 +1,290 @@ +import { useAtom } from "jotai"; +import { Label, Select } from "radix-ui"; +import * as React from "react"; + +import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; +import { useTraceContext } from "@/hooks/trace"; +import { funcAtom } from "@/state/func"; +import { type NormalizationMode, renderAtom } from "@/state/render"; +import { tabularDataAtom, type Scale } from "@/state/tabularData"; +import { threadAtom, NO_THREAD_INFO_SENTINEL_ID } from "@/state/thread"; + +function RenderModeParameters() { + const { funcs } = useTraceContext(); + const [activeFunc, setActiveFunc] = useAtom(funcAtom); + const [render, setRender] = useAtom(renderAtom); + const [tabularData, setTabularData] = useAtom(tabularDataAtom); + const [thread, setThread] = useAtom(threadAtom); + + const renderSecondaryControls = React.useCallback(() => { + switch (render.renderMode) { + case "Grayscale": + case "RGB": + return null; + case "Store Frequency": + case "Load Frequency": + case "Redundant Stores": + case "Reuse Distance": + return ( +
+
+ + Scale + + + setTabularData({ ...tabularData, scale: value as Scale }) + } + > + + + + + + + + + + Linear + + + Log + + + + +
+
+ + Normalize Display + + + setRender({ + ...render, + normalizationMode: value as NormalizationMode, + }) + } + > + + + + + + + + + + Across Funcs + + + Per Func + + + + +
+
+ ); + case "Thread Coverage": + return ( +
+
+ + Operation + + { + setThread({ ...thread, op: value as "Load" | "Store" }); + }} + > + + + + + + + + + + + + Store + + + Load + + + + +
+
+ + Filter by Thread + + { + setThread({ + ...thread, + id: value === "None" ? NO_THREAD_INFO_SENTINEL_ID : value, + }); + }} + > + + + + + + + + + + + + None + + {funcs[activeFunc].thread_ids + .filter((threadId) => threadId !== "0") + .map((threadId) => ( + + {threadId} + + ))} + + + +
+
+ ); + } + }, [ + render, + setRender, + tabularData, + setTabularData, + thread, + setThread, + funcs, + activeFunc, + ]); + + return ( +
+
+ + Selected Func + + + + + + + + + + + + + {Object.keys(funcs).map((func) => ( + + {func} + + ))} + + + +
+ {renderSecondaryControls()} +
+ ); +} + +export default RenderModeParameters; From c7d5dfe7ee017a3fb02152470bf4480fdb7cf6d1 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Fri, 7 Aug 2026 11:13:45 -0700 Subject: [PATCH 45/67] Only include NaN and Inf overlay buffers when explicitly requested. --- apps/halidoscope/src-tauri/src/commands.rs | 181 +++++++++++++---- apps/halidoscope/src-tauri/src/render.rs | 190 +++++++++++------- .../src/components/canvas/FuncNode.tsx | 4 +- apps/halidoscope/src/utils/api.ts | 56 ++++-- 4 files changed, 298 insertions(+), 133 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 8380581231bc..a2a2b0f18c27 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -10,7 +10,7 @@ use tauri::ipc::Response; use tauri::{AppHandle, Emitter, State}; use crate::render::{ - GrayscaleState, IncludeInf, IncludeNan, LoadFrequencyState, NormalizationMode, RedundantState, + GrayscaleState, InfState, LoadFrequencyState, NanState, NormalizationMode, RedundantState, Renderer, ReuseDistanceState, RgbState, StoreFrequencyState, ThreadOpMode, ThreadState, }; use crate::trace::Trace; @@ -171,14 +171,16 @@ pub struct AppState { /// Packs tensor data, tabular data, and NaN / Inf data in a single IPC response. fn pack_render_response( mut pixels: Vec, - nan_inf_overlays: Vec, - tabular_data: &[u32], + nan_overlay: Vec, + inf_overlay: Vec, + tabular_data: Vec, ) -> Vec { - pixels.reserve(nan_inf_overlays.len() + tabular_data.len() * 4); + pixels.reserve(nan_overlay.len() + inf_overlay.len() + tabular_data.len() * 4); - pixels.extend_from_slice(&nan_inf_overlays); + pixels.extend_from_slice(&nan_overlay); + pixels.extend_from_slice(&inf_overlay); - for &v in tabular_data { + for v in tabular_data { pixels.extend_from_slice(&v.to_le_bytes()); } @@ -235,8 +237,8 @@ pub fn render_grayscale( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: IncludeNan, - include_inf: IncludeInf, + include_nan: NanState, + include_inf: InfState, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -259,16 +261,30 @@ pub fn render_grayscale( renderer.seek(trace, store_indices, k); let pixels = renderer.to_rgba(normalization_mode); + + let nan_overlay = if include_nan.active { + renderer.to_nan_overlay(include_nan) + } else { + Vec::new() + }; + + let inf_overlay = if include_inf.active { + renderer.to_inf_overlay(include_inf) + } else { + Vec::new() + }; + let histogram = if include_tabular_data { renderer.to_histogram() } else { Vec::new() }; - let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( pixels, - nan_inf_overlays, - &histogram, + nan_overlay, + inf_overlay, + histogram, ))) } @@ -280,8 +296,8 @@ pub fn render_rgb( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: IncludeNan, - include_inf: IncludeInf, + include_nan: NanState, + include_inf: InfState, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -304,16 +320,30 @@ pub fn render_rgb( renderer.seek(trace, store_indices, k); let pixels = renderer.to_rgba(normalization_mode); + + let nan_overlay = if include_nan.active { + renderer.to_nan_overlay(include_nan) + } else { + Vec::new() + }; + + let inf_overlay = if include_inf.active { + renderer.to_inf_overlay(include_inf) + } else { + Vec::new() + }; + let histogram = if include_tabular_data { renderer.to_histogram() } else { Vec::new() }; - let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( pixels, - nan_inf_overlays, - &histogram, + nan_overlay, + inf_overlay, + histogram, ))) } @@ -324,8 +354,8 @@ pub fn render_store_frequency( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: IncludeNan, - include_inf: IncludeInf, + include_nan: NanState, + include_inf: InfState, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -350,16 +380,30 @@ pub fn render_store_frequency( renderer.seek(trace, store_indices, k); let pixels = renderer.to_rgba(normalization_mode); + + let nan_overlay = if include_nan.active { + renderer.to_nan_overlay(include_nan) + } else { + Vec::new() + }; + + let inf_overlay = if include_inf.active { + renderer.to_inf_overlay(include_inf) + } else { + Vec::new() + }; + let histogram = if include_tabular_data { renderer.to_tabular_data(normalization_mode) } else { Vec::new() }; - let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( pixels, - nan_inf_overlays, - &histogram, + nan_overlay, + inf_overlay, + histogram, ))) } @@ -370,8 +414,8 @@ pub fn render_load_frequency( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: IncludeNan, - include_inf: IncludeInf, + include_nan: NanState, + include_inf: InfState, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -396,17 +440,30 @@ pub fn render_load_frequency( renderer.seek(trace, load_indices, k); let pixels = renderer.to_rgba(normalization_mode); + + let nan_overlay = if include_nan.active { + renderer.to_nan_overlay(include_nan) + } else { + Vec::new() + }; + + let inf_overlay = if include_inf.active { + renderer.to_inf_overlay(include_inf) + } else { + Vec::new() + }; + let histogram = if include_tabular_data { renderer.to_tabular_data(normalization_mode) } else { Vec::new() }; - let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); Ok(Response::new(pack_render_response( pixels, - nan_inf_overlays, - &histogram, + nan_overlay, + inf_overlay, + histogram, ))) } @@ -419,8 +476,8 @@ pub fn render_redundant_stores( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: IncludeNan, - include_inf: IncludeInf, + include_nan: NanState, + include_inf: InfState, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -445,16 +502,30 @@ pub fn render_redundant_stores( renderer.seek(trace, store_indices, load_indices, store_k, load_k); let pixels = renderer.to_rgba(normalization_mode); + + let nan_overlay = if include_nan.active { + renderer.to_nan_overlay(include_nan) + } else { + Vec::new() + }; + + let inf_overlay = if include_inf.active { + renderer.to_inf_overlay(include_inf) + } else { + Vec::new() + }; + let histogram = if include_tabular_data { renderer.to_tabular_data(normalization_mode) } else { Vec::new() }; - let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( pixels, - nan_inf_overlays, - &histogram, + nan_overlay, + inf_overlay, + histogram, ))) } @@ -467,8 +538,8 @@ pub fn render_reuse_distance( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: IncludeNan, - include_inf: IncludeInf, + include_nan: NanState, + include_inf: InfState, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -495,16 +566,30 @@ pub fn render_reuse_distance( renderer.seek(trace, store_indices, load_indices, store_k, load_k); let pixels = renderer.to_rgba(normalization_mode); + + let nan_overlay = if include_nan.active { + renderer.to_nan_overlay(include_nan) + } else { + Vec::new() + }; + + let inf_overlay = if include_inf.active { + renderer.to_inf_overlay(include_inf) + } else { + Vec::new() + }; + let histogram = if include_tabular_data { renderer.to_tabular_data(normalization_mode) } else { Vec::new() }; - let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( pixels, - nan_inf_overlays, - &histogram, + nan_overlay, + inf_overlay, + histogram, ))) } @@ -514,8 +599,8 @@ pub fn render_thread( global_index: u32, op_mode: ThreadOpMode, thread_id: String, - include_nan: IncludeNan, - include_inf: IncludeInf, + include_nan: NanState, + include_inf: InfState, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -539,13 +624,27 @@ pub fn render_thread( renderer.seek(trace, store_indices, load_indices, store_k, load_k, op_mode); let pixels = renderer.to_rgba(thread_id); + + let nan_overlay = if include_nan.active { + renderer.to_nan_overlay(include_nan) + } else { + Vec::new() + }; + + let inf_overlay = if include_inf.active { + renderer.to_inf_overlay(include_inf) + } else { + Vec::new() + }; + let (store_counts, load_counts) = renderer.to_thread_counts(); let thread_counts: Vec = store_counts.iter().chain(load_counts).copied().collect(); - let nan_inf_overlays = renderer.to_nan_inf_overlay(include_nan, include_inf); + Ok(Response::new(pack_render_response( pixels, - nan_inf_overlays, - &thread_counts, + nan_overlay, + inf_overlay, + thread_counts, ))) } diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index a28a445a78dd..d5f9f3d4f8cb 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -15,7 +15,7 @@ pub enum NormalizationMode { } #[derive(Deserialize, Clone, Copy)] -pub struct IncludeNan { +pub struct NanState { pub active: bool, pub r: u8, pub g: u8, @@ -23,10 +23,8 @@ pub struct IncludeNan { pub a: f64, } -/// The frontend's Inf overlay toggle and color, `infAtom` in `state/inf.ts`. `r`/`g`/`b` are -/// 8-bit channels; `a` is a [0, 1] fraction converted to an 8-bit alpha when painting the overlay. #[derive(Deserialize, Clone, Copy)] -pub struct IncludeInf { +pub struct InfState { pub active: bool, pub r: u8, pub g: u8, @@ -40,54 +38,53 @@ pub trait Renderer: Sized { fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize); fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec; - fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec; + fn to_nan_overlay(&self, nan_state: NanState) -> Vec; + fn to_inf_overlay(&self, inf_state: InfState) -> Vec; fn to_values(&self) -> Vec; } -/// Packs a NaN overlay (colored per `include_nan`) followed by an Inf overlay (colored per -/// `include_inf`) into one RGBA8 buffer, `width * height * 4` bytes each, from a renderer's -/// per-`(pixel, channel)` decoded values. -fn nan_inf_overlay( +fn nan_overlay( values: &[f64], width: usize, height: usize, channels: usize, - include_nan: IncludeNan, - include_inf: IncludeInf, + nan_state: NanState, ) -> Vec { let overlay_len = width * height * 4; - let mut out = vec![0u8; overlay_len * 2]; - let (nan_out, inf_out) = out.split_at_mut(overlay_len); + let mut out = vec![0u8; overlay_len]; - if include_nan.active { - let alpha = (include_nan.a * 255.0).clamp(0.0, 255.0) as u8; + let alpha = (nan_state.a * 255.0).clamp(0.0, 255.0) as u8; - for (chunk, src) in nan_out - .chunks_exact_mut(4) - .zip(values.chunks_exact(channels)) - { - if src.iter().any(|v| v.is_nan()) { - chunk[0] = include_nan.r; - chunk[1] = include_nan.g; - chunk[2] = include_nan.b; - chunk[3] = alpha; - } + for (chunk, src) in out.chunks_exact_mut(4).zip(values.chunks_exact(channels)) { + if src.iter().any(|v| v.is_nan()) { + chunk[0] = nan_state.r; + chunk[1] = nan_state.g; + chunk[2] = nan_state.b; + chunk[3] = alpha; } } - if include_inf.active { - let alpha = (include_inf.a * 255.0).clamp(0.0, 255.0) as u8; + out +} - for (chunk, src) in inf_out - .chunks_exact_mut(4) - .zip(values.chunks_exact(channels)) - { - if src.iter().any(|v| v.is_infinite()) { - chunk[0] = include_inf.r; - chunk[1] = include_inf.g; - chunk[2] = include_inf.b; - chunk[3] = alpha; - } +fn inf_overlay( + values: &[f64], + width: usize, + height: usize, + channels: usize, + inf_state: InfState, +) -> Vec { + let overlay_len = width * height * 4; + let mut out = vec![0u8; overlay_len]; + + let alpha = (inf_state.a * 255.0).clamp(0.0, 255.0) as u8; + + for (chunk, src) in out.chunks_exact_mut(4).zip(values.chunks_exact(channels)) { + if src.iter().any(|v| v.is_infinite()) { + chunk[0] = inf_state.r; + chunk[1] = inf_state.g; + chunk[2] = inf_state.b; + chunk[3] = alpha; } } @@ -237,7 +234,7 @@ impl Renderer for GrayscaleState { self.values.clone() } - fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { + fn to_nan_overlay(&self, nan_state: NanState) -> Vec { let FuncGeometry { width, height, @@ -245,14 +242,18 @@ impl Renderer for GrayscaleState { .. } = self.geom; - nan_inf_overlay( - &self.values, + nan_overlay(&self.values, width, height, channels, nan_state) + } + + fn to_inf_overlay(&self, inf_state: InfState) -> Vec { + let FuncGeometry { width, height, channels, - include_nan, - include_inf, - ) + .. + } = self.geom; + + inf_overlay(&self.values, width, height, channels, inf_state) } } @@ -404,7 +405,7 @@ impl Renderer for RgbState { self.values.clone() } - fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { + fn to_nan_overlay(&self, nan_state: NanState) -> Vec { let FuncGeometry { width, height, @@ -412,14 +413,18 @@ impl Renderer for RgbState { .. } = self.geom; - nan_inf_overlay( - &self.values, + nan_overlay(&self.values, width, height, channels, nan_state) + } + + fn to_inf_overlay(&self, inf_state: InfState) -> Vec { + let FuncGeometry { width, height, channels, - include_nan, - include_inf, - ) + .. + } = self.geom; + + inf_overlay(&self.values, width, height, channels, inf_state) } } @@ -562,7 +567,7 @@ impl Renderer for StoreFrequencyState { self.counts.clone() } - fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { + fn to_nan_overlay(&self, nan_state: NanState) -> Vec { let FuncGeometry { width, height, @@ -570,14 +575,18 @@ impl Renderer for StoreFrequencyState { .. } = self.geom; - nan_inf_overlay( - &self.values, + nan_overlay(&self.values, width, height, channels, nan_state) + } + + fn to_inf_overlay(&self, inf_state: InfState) -> Vec { + let FuncGeometry { width, height, channels, - include_nan, - include_inf, - ) + .. + } = self.geom; + + inf_overlay(&self.values, width, height, channels, inf_state) } } @@ -719,7 +728,7 @@ impl Renderer for LoadFrequencyState { self.counts.clone() } - fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { + fn to_nan_overlay(&self, nan_state: NanState) -> Vec { let FuncGeometry { width, height, @@ -727,14 +736,18 @@ impl Renderer for LoadFrequencyState { .. } = self.geom; - nan_inf_overlay( - &self.values, + nan_overlay(&self.values, width, height, channels, nan_state) + } + + fn to_inf_overlay(&self, inf_state: InfState) -> Vec { + let FuncGeometry { width, height, channels, - include_nan, - include_inf, - ) + .. + } = self.geom; + + inf_overlay(&self.values, width, height, channels, inf_state) } } @@ -947,20 +960,38 @@ impl RedundantState { self.redundant_store_counts.clone() } - pub fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { + pub fn to_nan_overlay(&self, nan_state: NanState) -> Vec { + let FuncGeometry { + width, + height, + channels, + .. + } = self.geom; + + let values: Vec = self + .last_values + .iter() + .map(|v| v.map_or(0.0, f64::from_bits)) + .collect(); + + nan_overlay(&values, width, height, channels, nan_state) + } + + pub fn to_inf_overlay(&self, inf_state: InfState) -> Vec { let FuncGeometry { width, height, channels, .. } = self.geom; + let values: Vec = self .last_values .iter() .map(|v| v.map_or(0.0, f64::from_bits)) .collect(); - nan_inf_overlay(&values, width, height, channels, include_nan, include_inf) + inf_overlay(&values, width, height, channels, inf_state) } } @@ -1187,7 +1218,7 @@ impl ReuseDistanceState { self.max_reuse_distance.clone() } - pub fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { + pub fn to_nan_overlay(&self, nan_state: NanState) -> Vec { let FuncGeometry { width, height, @@ -1195,14 +1226,18 @@ impl ReuseDistanceState { .. } = self.geom; - nan_inf_overlay( - &self.values, + nan_overlay(&self.values, width, height, channels, nan_state) + } + + pub fn to_inf_overlay(&self, inf_state: InfState) -> Vec { + let FuncGeometry { width, height, channels, - include_nan, - include_inf, - ) + .. + } = self.geom; + + inf_overlay(&self.values, width, height, channels, inf_state) } } @@ -1424,20 +1459,25 @@ impl ThreadState { (&self.store_counts, &self.load_counts) } - pub fn to_nan_inf_overlay(&self, include_nan: IncludeNan, include_inf: IncludeInf) -> Vec { + pub fn to_nan_overlay(&self, nan_state: NanState) -> Vec { let FuncGeometry { width, height, channels, .. } = self.geom; - nan_inf_overlay( - &self.values, + + nan_overlay(&self.values, width, height, channels, nan_state) + } + + pub fn to_inf_overlay(&self, inf_state: InfState) -> Vec { + let FuncGeometry { width, height, channels, - include_nan, - include_inf, - ) + .. + } = self.geom; + + inf_overlay(&self.values, width, height, channels, inf_state) } } diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 693e9df72fb4..570dfaca8f0b 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -173,7 +173,7 @@ function FuncNode({ data }: NodeProps>) { } const nanCtx = nanOverlayRef.current?.getContext("2d"); - if (nanCtx) { + if (nanCtx && result.nanOverlayData) { nanCtx.putImageData( new ImageData(result.nanOverlayData, width, height), 0, @@ -182,7 +182,7 @@ function FuncNode({ data }: NodeProps>) { } const infCtx = infOverlayRef.current?.getContext("2d"); - if (infCtx) { + if (infCtx && result.infOverlayData) { infCtx.putImageData( new ImageData(result.infOverlayData, width, height), 0, diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index ee27cc3ba94c..33d956a3f9cf 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -21,9 +21,9 @@ export interface RenderFuncResponse { /** The rendered RGBA8 tensor data for the Func's buffer. */ tensorData: Uint8ClampedArray; /** The RGBA8 overlay marking coordinates where a NaN was observed. */ - nanOverlayData: Uint8ClampedArray; + nanOverlayData: Uint8ClampedArray | null; /** The RGBA8 overlay marking coordinates where an Inf was observed. */ - infOverlayData: Uint8ClampedArray; + infOverlayData: Uint8ClampedArray | null; /** * The per-coordinate tabular data backing histograms, or null if not * requested. @@ -79,6 +79,10 @@ export interface RenderFuncParams { * @param buffer The buffer containing tensor, tabular, and overlay data. * @param width The width of the buffer. * @param height The height of the buffer. + * @param includeNan A flag indicating whether or not to expect a NaN overlay + * in the buffer payload. + * @param includeInf A flag indicating whether or not to expect an Inf overlay + * in the buffer payload. * @param includeTabularData A flag indicating whether or not to expect tabular * data in the buffer payload. * @returns A {@link RenderFuncResponse}. @@ -87,34 +91,42 @@ function splitRenderBuffer({ buffer, width, height, + includeNan, + includeInf, includeTabularData, }: { buffer: ArrayBuffer; width: number; height: number; + includeNan: boolean; + includeInf: boolean; includeTabularData: boolean; }): RenderFuncResponse { const pixelByteLength = width * height * 4; const overlayPlaneByteLength = width * height * 4; - const tabularByteLength = - buffer.byteLength - pixelByteLength - overlayPlaneByteLength * 2; + const overlayBytes = + (includeNan ? overlayPlaneByteLength : 0) + + (includeInf ? overlayPlaneByteLength : 0); + const tabularByteLength = buffer.byteLength - pixelByteLength - overlayBytes; return { tensorData: new Uint8ClampedArray(buffer, 0, pixelByteLength), - nanOverlayData: new Uint8ClampedArray( - buffer, - pixelByteLength, - overlayPlaneByteLength, - ), - infOverlayData: new Uint8ClampedArray( - buffer, - pixelByteLength + overlayPlaneByteLength, - overlayPlaneByteLength, - ), + nanOverlayData: includeNan + ? new Uint8ClampedArray(buffer, pixelByteLength, overlayPlaneByteLength) + : null, + infOverlayData: includeInf + ? new Uint8ClampedArray( + buffer, + pixelByteLength + (includeNan ? overlayPlaneByteLength : 0), + overlayPlaneByteLength, + ) + : null, tabularData: includeTabularData ? new Uint32Array( buffer, - pixelByteLength + 2 * overlayPlaneByteLength, + pixelByteLength + + (includeNan ? overlayPlaneByteLength : 0) + + (includeInf ? overlayPlaneByteLength : 0), tabularByteLength / 4, ) : null, @@ -151,6 +163,8 @@ export async function renderGrayscale({ buffer, width, height, + includeNan: includeNan.active, + includeInf: includeInf.active, includeTabularData, }); } @@ -186,6 +200,8 @@ export async function renderRgb({ buffer, width, height, + includeNan: includeNan.active, + includeInf: includeInf.active, includeTabularData, }); } @@ -220,6 +236,8 @@ export async function renderStoreFrequency({ buffer, width, height, + includeNan: includeNan.active, + includeInf: includeInf.active, includeTabularData, }); } @@ -254,6 +272,8 @@ export async function renderLoadFrequency({ buffer, width, height, + includeNan: includeNan.active, + includeInf: includeInf.active, includeTabularData, }); } @@ -294,6 +314,8 @@ export async function renderRedundantStores({ buffer, width, height, + includeNan: includeNan.active, + includeInf: includeInf.active, includeTabularData, }); } @@ -335,6 +357,8 @@ export async function renderReuseDistance({ buffer, width, height, + includeNan: includeNan.active, + includeInf: includeInf.active, includeTabularData, }); } @@ -383,6 +407,8 @@ export async function renderThread({ buffer, width, height, + includeNan: includeNan.active, + includeInf: includeInf.active, includeTabularData, }); } From 592ccd7c96f5670bd85ec368700706e9c213e739 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Fri, 7 Aug 2026 14:27:47 -0700 Subject: [PATCH 46/67] Only request live Funcs during playback to reduce IPC calls. --- .../src/components/canvas/FuncNode.tsx | 128 +++++++++++------- .../src/components/controls/FuncsPanel.tsx | 6 +- .../controls/VisualizationPanel.tsx | 10 +- 3 files changed, 88 insertions(+), 56 deletions(-) diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 570dfaca8f0b..0f9321a3c775 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -38,7 +38,7 @@ import { import { isFuncBufferLive, isEdgeLive } from "@/utils/liveness"; function FuncNode({ data }: NodeProps>) { - const { name, width, height } = data; + const { name, width, height, buffer_liveness, max_store_count } = data; const canvasRef = React.useRef(null); const nanOverlayRef = React.useRef(null); const infOverlayRef = React.useRef(null); @@ -111,56 +111,82 @@ function FuncNode({ data }: NodeProps>) { return; } + renderingRef.current = true; + async function draw() { try { + // Funcs with no stores (pipeline inputs) never see a Begin/EndRealization + // pair, so `buffer_liveness` defaults to (0, 0) rather than a real teardown + // point — treat those as always live instead of "dead" past index 0. + const isRealized = max_store_count > 0; + + // Reuse cached buffers for a Func outside of its liveness range. + let cachedPreLiveResult: RenderFuncResponse | null = null; + let cachedPostLiveResult: RenderFuncResponse | null = null; + while (true) { const target = latestIndexRef.current; + const notYetLive = target < buffer_liveness.start; + const noLongerLive = isRealized && target > buffer_liveness.end; let result: RenderFuncResponse; - const params: RenderFuncParams = { - func: name, - globalIndex: target, - normalizationMode: render.normalizationMode, - width, - height, - includeTabularData: active, - includeNan: { - active: nan.active, - ...nan.color, - }, - includeInf: { - active: inf.active, - ...inf.color, - }, - }; - switch (render.renderMode) { - case "Grayscale": - result = await renderGrayscale(params); - break; - case "RGB": - result = await renderRgb(params); - break; - case "Store Frequency": - result = await renderStoreFrequency(params); - break; - case "Load Frequency": - result = await renderLoadFrequency(params); - break; - case "Redundant Stores": - result = await renderRedundantStores(params); - break; - case "Reuse Distance": - result = await renderReuseDistance(params); - break; - case "Thread Coverage": - result = await renderThread({ - ...params, - threadOpMode: thread.op, - threadId: thread.id, - }); + if (notYetLive && cachedPreLiveResult) { + result = cachedPreLiveResult; + } else if (noLongerLive && cachedPostLiveResult) { + result = cachedPostLiveResult; + } else { + const params: RenderFuncParams = { + func: name, + globalIndex: target, + normalizationMode: render.normalizationMode, + width, + height, + includeTabularData: active, + includeNan: { + active: nan.active, + ...nan.color, + }, + includeInf: { + active: inf.active, + ...inf.color, + }, + }; - break; + switch (render.renderMode) { + case "Grayscale": + result = await renderGrayscale(params); + break; + case "RGB": + result = await renderRgb(params); + break; + case "Store Frequency": + result = await renderStoreFrequency(params); + break; + case "Load Frequency": + result = await renderLoadFrequency(params); + break; + case "Redundant Stores": + result = await renderRedundantStores(params); + break; + case "Reuse Distance": + result = await renderReuseDistance(params); + break; + case "Thread Coverage": + result = await renderThread({ + ...params, + threadOpMode: thread.op, + threadId: thread.id, + }); + break; + } + + // Set cached versions if we fall outside a Funcs buffer range. + if (notYetLive) { + cachedPreLiveResult = result; + } else if (noLongerLive) { + cachedPostLiveResult = result; + } } const ctx = canvasRef.current?.getContext("2d"); @@ -206,6 +232,8 @@ function FuncNode({ data }: NodeProps>) { console.error( `Failed to render ${name} at index ${latestIndexRef.current}: ${err}`, ); + } finally { + renderingRef.current = false; } } @@ -222,6 +250,9 @@ function FuncNode({ data }: NodeProps>) { thread, nan, inf, + buffer_liveness.start, + buffer_liveness.end, + max_store_count, ]); return ( @@ -235,13 +266,10 @@ function FuncNode({ data }: NodeProps>) { className="truncate" > = 0.5, - }, - )} + className={clsx("text-ps-text-primary font-mono whitespace-nowrap", { + "text-tiny": zoom < 0.5, + "text-xs": zoom >= 0.5, + })} > {name} diff --git a/apps/halidoscope/src/components/controls/FuncsPanel.tsx b/apps/halidoscope/src/components/controls/FuncsPanel.tsx index 70a051604d9a..0b4712d524cd 100644 --- a/apps/halidoscope/src/components/controls/FuncsPanel.tsx +++ b/apps/halidoscope/src/components/controls/FuncsPanel.tsx @@ -25,15 +25,15 @@ function FuncsPanel({ funcs }: FuncsPanelProps) { value={func.name} className="group flex flex-col" > - + - + {func.name} diff --git a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx index 9feef9de7a31..a5a0a73757f7 100644 --- a/apps/halidoscope/src/components/controls/VisualizationPanel.tsx +++ b/apps/halidoscope/src/components/controls/VisualizationPanel.tsx @@ -173,8 +173,8 @@ function VisualizationPanel() { return { type: "Histogram", data, - domain, - renderLegend: true, + domain: [domain[0], domain[1] + step], + renderLegend: false, }; }, [], @@ -311,7 +311,11 @@ function VisualizationPanel() { Date: Mon, 10 Aug 2026 09:36:29 -0700 Subject: [PATCH 47/67] Add more granular loading feedback to support XL traces. --- apps/halidoscope/src-tauri/src/cli.rs | 8 +-- apps/halidoscope/src-tauri/src/commands.rs | 18 +++--- apps/halidoscope/src-tauri/src/trace.rs | 63 +++++++++++++++---- apps/halidoscope/src/App.tsx | 39 +++++++++--- .../components/views/trace/TraceLoading.tsx | 5 +- 5 files changed, 101 insertions(+), 32 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/cli.rs b/apps/halidoscope/src-tauri/src/cli.rs index 675aac9569ca..41a5b36d55e5 100644 --- a/apps/halidoscope/src-tauri/src/cli.rs +++ b/apps/halidoscope/src-tauri/src/cli.rs @@ -34,7 +34,7 @@ fn dot(subcommand: SubcommandMatches) -> Option<()> { let destination = args.get("destination").and_then(|a| a.value.as_str()); // Load and parse the trace. - let trace = Trace::load_from_file(trace_path, |_| {}).unwrap_or_else(|e| { + let trace = Trace::load_from_file(trace_path, |_, _| {}).unwrap_or_else(|e| { eprintln!("Error loading trace: {}", e); std::process::exit(1); }); @@ -75,7 +75,7 @@ fn list(subcommand: SubcommandMatches) -> Option<()> { let trace_path = args.get("trace").and_then(|a| a.value.as_str())?; // Load and parse the trace. - let trace = Trace::load_from_file(trace_path, |_| {}).unwrap_or_else(|e| { + let trace = Trace::load_from_file(trace_path, |_, _| {}).unwrap_or_else(|e| { eprintln!("Error loading trace: {}", e); std::process::exit(1); }); @@ -129,7 +129,7 @@ fn stats(subcommand: SubcommandMatches) -> Option<()> { let func = args.get("func").and_then(|a| a.value.as_str()); // Load and parse the trace. - let trace = Trace::load_from_file(trace_path, |_| {}).unwrap_or_else(|e| { + let trace = Trace::load_from_file(trace_path, |_, _| {}).unwrap_or_else(|e| { eprintln!("Error loading trace: {}", e); std::process::exit(1); }); @@ -230,7 +230,7 @@ fn snapshot(subcommand: SubcommandMatches) -> Option<()> { let destination = args.get("destination").and_then(|a| a.value.as_str())?; // Load and parse the trace. - let trace = Trace::load_from_file(trace_path, |_| {}).unwrap_or_else(|e| { + let trace = Trace::load_from_file(trace_path, |_, _| {}).unwrap_or_else(|e| { eprintln!("Error loading trace: {}", e); std::process::exit(1); }); diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index a2a2b0f18c27..309e3609e171 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -189,14 +189,18 @@ fn pack_render_response( // ── Commands ────────────────────────────────────────────────────────────────── +#[derive(Debug, Clone, Serialize)] +struct TraceProgress { + message: String, + progress: u8, +} + /// Parses a `.hltrace` file and returns the metadata the frontend needs to set up canvases and /// the scrub timeline. Replaces any previously loaded trace. /// -/// Runs the parse on a blocking-task thread rather than the main thread: `open_trace` isn't -/// declared `async`, so a plain `fn` command would otherwise execute inline on the thread that -/// pumps the webview's event loop, freezing the UI (including any in-progress loading indicator) -/// for the duration of the parse. Progress (percentage of bytes parsed) is emitted to the -/// frontend as `trace-load-progress` events. +/// Runs the parse on a blocking-task thread rather than the main thread. Running async on a +/// separate thread prevents us from blocking the webview's event loop and freezing the UI. +/// Progress (percentage of bytes parsed) is emitted to the frontend as `trace-load-progress` events. #[tauri::command] pub async fn open_trace( path: String, @@ -204,8 +208,8 @@ pub async fn open_trace( state: State<'_, AppState>, ) -> Result { let (trace, meta) = tauri::async_runtime::spawn_blocking(move || { - let trace = Trace::load_from_file(&path, |pct| { - let _ = app.emit("trace-load-progress", pct); + let trace = Trace::load_from_file(&path, |message, progress| { + let _ = app.emit("trace-load-progress", TraceProgress { message, progress }); })?; let meta = TraceMeta::from_trace(&trace); diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 8ca439def3ec..7a98867a7e60 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -403,12 +403,15 @@ fn parse_func_type_and_dim( // ── Trace loading ──────────────────────────────────────────────────────────────────────────────── impl Trace { - pub fn load_from_file(path: &str, on_progress: impl FnMut(u8)) -> Result { + pub fn load_from_file(path: &str, on_progress: impl FnMut(String, u8)) -> Result { let data = std::fs::read(path).map_err(|e| e.to_string())?; Self::load_from_bytes(&data, on_progress) } - pub fn load_from_bytes(data: &[u8], mut on_progress: impl FnMut(u8)) -> Result { + pub fn load_from_bytes( + data: &[u8], + mut on_progress: impl FnMut(String, u8), + ) -> Result { let total = data.len(); let mut pos = 0; let mut last_reported_pct: u8 = 0; @@ -661,19 +664,21 @@ impl Trace { packets.push(pkt); pos += size; - // Intentionally max pct at 95 to support movement to 100 after all stats below are - // computed. - let pct = (pos as u64 * 100 / total.max(1) as u64).max(95) as u8; + let pct = (pos as u64 * 100 / total.max(1) as u64) as u8; if pct > last_reported_pct { last_reported_pct = pct; - on_progress(pct); + on_progress("Loading trace...".to_string(), pct); } } // ── DAG inference ──────────────────────────────────────────────────────────────────────── // Walk up the parent chain from each load to find the enclosing Produce event; that - // Produce's func is a producer of the loaded func. - for (func_name, load_parent_id) in &pending_loads { + // Produce's func is a producer of the loaded func. This walk is O(pending_loads * chain + // depth) and pending_loads has one entry per Load packet, so it can take a while on large + // traces — report progress through it rather than sitting silent until it's done. + let total_pending_loads = pending_loads.len(); + let mut dag_last_reported_pct: u8 = 0; + for (index, (func_name, load_parent_id)) in pending_loads.iter().enumerate() { let loaded_func = func_name.clone(); let mut current = *load_parent_id; @@ -692,12 +697,28 @@ impl Trace { None => break, } } + + let pct = (index as u64 * 25 / total_pending_loads.max(1) as u64) as u8; + if pct > dag_last_reported_pct { + dag_last_reported_pct = pct; + on_progress("Analyzing trace...".to_string(), pct); + } } + on_progress("Analyzing trace...".to_string(), 25); + // Resolve the executing thread for each load/store packet by walking its parent chain up // to the nearest enclosing `BeginParallelTask`. Exclude any stores / loads that do not // have a meaningful thread_id (denoted by a sentinel value of -1). - for pkt in packets.iter_mut() { + let total_packets = packets.len(); + let mut thread_last_reported_pct: u8 = 25; + for (index, pkt) in packets.iter_mut().enumerate() { + let pct = 25 + (index as u64 * 25 / total_packets.max(1) as u64) as u8; + if pct > thread_last_reported_pct { + thread_last_reported_pct = pct; + on_progress("Analyzing trace...".to_string(), pct); + } + if !pkt.is_load_or_store() { continue; } @@ -737,7 +758,9 @@ impl Trace { // // We extract extents/channels first (shared borrow) then write back (mut borrow) to keep // the two borrows of `funcs` non-overlapping. - for (func_name, store_indices) in &store_indices_by_func { + let total_store_funcs = store_indices_by_func.len(); + let mut merge_last_reported_pct: u8 = 50; + for (index, (func_name, store_indices)) in store_indices_by_func.iter().enumerate() { let extents = funcs.get(func_name.as_str()).and_then(func_extents); if let Some((w, h, min_x, min_y)) = extents { let stats = funcs.get(func_name.as_str()).unwrap(); @@ -862,13 +885,29 @@ impl Trace { global_max_reuse_distance.max(stats.max_reuse_distance); } } + + let pct = 50 + (index as u64 * 25 / total_store_funcs.max(1) as u64) as u8; + if pct > merge_last_reported_pct { + merge_last_reported_pct = pct; + on_progress("Analyzing trace...".to_string(), pct); + } } + on_progress("Analyzing trace...".to_string(), 75); + // Pipeline inputs: Funcs with loads but no stores. These aren't covered by the merged // loop above, so compute their load count and reuse distance in one pass here. The first // load at each (x, y, channel) is free (analogous to a memcpy); subsequent loads measure // distance from that first load. - for (func_name, load_indices) in &load_indices_by_func { + let total_load_funcs = load_indices_by_func.len(); + let mut load_funcs_last_reported_pct: u8 = 75; + for (index, (func_name, load_indices)) in load_indices_by_func.iter().enumerate() { + let pct = 75 + (index as u64 * 25 / total_load_funcs.max(1) as u64) as u8; + if pct > load_funcs_last_reported_pct { + load_funcs_last_reported_pct = pct; + on_progress("Analyzing trace...".to_string(), pct); + } + if store_indices_by_func.contains_key(func_name.as_str()) { continue; // handled by the merged loop above } @@ -934,7 +973,7 @@ impl Trace { } } - on_progress(100); + on_progress("Analyzing trace...".to_string(), 100); Ok(Self { packets, diff --git a/apps/halidoscope/src/App.tsx b/apps/halidoscope/src/App.tsx index b3f445da0564..6a552c7f9516 100644 --- a/apps/halidoscope/src/App.tsx +++ b/apps/halidoscope/src/App.tsx @@ -45,8 +45,13 @@ function App() { // Loading state. const [traceLoading, setTraceLoading] = React.useState<{ state: TraceLoadingState; + message: string; progress: number; - }>({ state: TraceLoadingState.Loading, progress: 0 }); + }>({ + state: TraceLoadingState.Loading, + message: "Loading trace...", + progress: 0, + }); // Trace state. const [funcs, setFuncs] = React.useState>({}); @@ -68,12 +73,23 @@ function App() { const loadTrace = React.useCallback( async (path: string) => { - setTraceLoading({ state: TraceLoadingState.Loading, progress: 0 }); - - const unlisten = await listen("trace-load-progress", (event) => { - setTraceLoading((prev) => ({ ...prev, progress: event.payload })); + setTraceLoading({ + state: TraceLoadingState.Loading, + message: "Loading trace...", + progress: 0, }); + const unlisten = await listen<{ progress: number; message: string }>( + "trace-load-progress", + (event) => { + setTraceLoading((prev) => ({ + ...prev, + progress: event.payload.progress, + message: event.payload.message, + })); + }, + ); + try { const { funcs, total_packets, dag_edges, stats } = await openTrace(path); @@ -90,7 +106,11 @@ function App() { setActiveFunc(funcs[0]?.name ?? ""); } finally { unlisten(); - setTraceLoading({ state: TraceLoadingState.Loaded, progress: 100 }); + setTraceLoading({ + state: TraceLoadingState.Loaded, + message: "Trace loaded...", + progress: 100, + }); } }, [setActiveFunc], @@ -149,7 +169,12 @@ function App() { const renderTrace = React.useCallback(() => { switch (traceLoading.state) { case TraceLoadingState.Loading: - return ; + return ( + + ); case TraceLoadingState.NeedsUpload: return ; case TraceLoadingState.Loaded: diff --git a/apps/halidoscope/src/components/views/trace/TraceLoading.tsx b/apps/halidoscope/src/components/views/trace/TraceLoading.tsx index 5114a4b0ecb0..e04f9f0914b5 100644 --- a/apps/halidoscope/src/components/views/trace/TraceLoading.tsx +++ b/apps/halidoscope/src/components/views/trace/TraceLoading.tsx @@ -1,13 +1,14 @@ import { motion } from "motion/react"; interface Props { + message: string; progress: number; } -function TraceLoading({ progress }: Props) { +function TraceLoading({ message, progress }: Props) { return (
-

Loading trace...

+

{message}

Date: Mon, 10 Aug 2026 21:56:09 -0700 Subject: [PATCH 48/67] Send NaN and Inf buffers as bit masks. --- apps/halidoscope/src-tauri/src/commands.rs | 88 ++++---- apps/halidoscope/src-tauri/src/render.rs | 232 +++++---------------- apps/halidoscope/src/utils/api.ts | 146 ++++++++----- 3 files changed, 186 insertions(+), 280 deletions(-) diff --git a/apps/halidoscope/src-tauri/src/commands.rs b/apps/halidoscope/src-tauri/src/commands.rs index 309e3609e171..864a74550df4 100644 --- a/apps/halidoscope/src-tauri/src/commands.rs +++ b/apps/halidoscope/src-tauri/src/commands.rs @@ -10,8 +10,8 @@ use tauri::ipc::Response; use tauri::{AppHandle, Emitter, State}; use crate::render::{ - GrayscaleState, InfState, LoadFrequencyState, NanState, NormalizationMode, RedundantState, - Renderer, ReuseDistanceState, RgbState, StoreFrequencyState, ThreadOpMode, ThreadState, + GrayscaleState, LoadFrequencyState, NormalizationMode, RedundantState, Renderer, + ReuseDistanceState, RgbState, StoreFrequencyState, ThreadOpMode, ThreadState, }; use crate::trace::Trace; @@ -241,8 +241,8 @@ pub fn render_grayscale( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: NanState, - include_inf: InfState, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -266,14 +266,14 @@ pub fn render_grayscale( let pixels = renderer.to_rgba(normalization_mode); - let nan_overlay = if include_nan.active { - renderer.to_nan_overlay(include_nan) + let nan_overlay = if include_nan { + renderer.to_nan_overlay() } else { Vec::new() }; - let inf_overlay = if include_inf.active { - renderer.to_inf_overlay(include_inf) + let inf_overlay = if include_inf { + renderer.to_inf_overlay() } else { Vec::new() }; @@ -300,8 +300,8 @@ pub fn render_rgb( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: NanState, - include_inf: InfState, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -325,14 +325,14 @@ pub fn render_rgb( let pixels = renderer.to_rgba(normalization_mode); - let nan_overlay = if include_nan.active { - renderer.to_nan_overlay(include_nan) + let nan_overlay = if include_nan { + renderer.to_nan_overlay() } else { Vec::new() }; - let inf_overlay = if include_inf.active { - renderer.to_inf_overlay(include_inf) + let inf_overlay = if include_inf { + renderer.to_inf_overlay() } else { Vec::new() }; @@ -358,8 +358,8 @@ pub fn render_store_frequency( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: NanState, - include_inf: InfState, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -385,14 +385,14 @@ pub fn render_store_frequency( let pixels = renderer.to_rgba(normalization_mode); - let nan_overlay = if include_nan.active { - renderer.to_nan_overlay(include_nan) + let nan_overlay = if include_nan { + renderer.to_nan_overlay() } else { Vec::new() }; - let inf_overlay = if include_inf.active { - renderer.to_inf_overlay(include_inf) + let inf_overlay = if include_inf { + renderer.to_inf_overlay() } else { Vec::new() }; @@ -418,8 +418,8 @@ pub fn render_load_frequency( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: NanState, - include_inf: InfState, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -445,14 +445,14 @@ pub fn render_load_frequency( let pixels = renderer.to_rgba(normalization_mode); - let nan_overlay = if include_nan.active { - renderer.to_nan_overlay(include_nan) + let nan_overlay = if include_nan { + renderer.to_nan_overlay() } else { Vec::new() }; - let inf_overlay = if include_inf.active { - renderer.to_inf_overlay(include_inf) + let inf_overlay = if include_inf { + renderer.to_inf_overlay() } else { Vec::new() }; @@ -480,8 +480,8 @@ pub fn render_redundant_stores( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: NanState, - include_inf: InfState, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -507,14 +507,14 @@ pub fn render_redundant_stores( let pixels = renderer.to_rgba(normalization_mode); - let nan_overlay = if include_nan.active { - renderer.to_nan_overlay(include_nan) + let nan_overlay = if include_nan { + renderer.to_nan_overlay() } else { Vec::new() }; - let inf_overlay = if include_inf.active { - renderer.to_inf_overlay(include_inf) + let inf_overlay = if include_inf { + renderer.to_inf_overlay() } else { Vec::new() }; @@ -542,8 +542,8 @@ pub fn render_reuse_distance( global_index: u32, normalization_mode: NormalizationMode, include_tabular_data: bool, - include_nan: NanState, - include_inf: InfState, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -571,14 +571,14 @@ pub fn render_reuse_distance( let pixels = renderer.to_rgba(normalization_mode); - let nan_overlay = if include_nan.active { - renderer.to_nan_overlay(include_nan) + let nan_overlay = if include_nan { + renderer.to_nan_overlay() } else { Vec::new() }; - let inf_overlay = if include_inf.active { - renderer.to_inf_overlay(include_inf) + let inf_overlay = if include_inf { + renderer.to_inf_overlay() } else { Vec::new() }; @@ -603,8 +603,8 @@ pub fn render_thread( global_index: u32, op_mode: ThreadOpMode, thread_id: String, - include_nan: NanState, - include_inf: InfState, + include_nan: bool, + include_inf: bool, state: State, ) -> Result { let mut guard = state.inner.lock().map_err(|e| e.to_string())?; @@ -629,14 +629,14 @@ pub fn render_thread( let pixels = renderer.to_rgba(thread_id); - let nan_overlay = if include_nan.active { - renderer.to_nan_overlay(include_nan) + let nan_overlay = if include_nan { + renderer.to_nan_overlay() } else { Vec::new() }; - let inf_overlay = if include_inf.active { - renderer.to_inf_overlay(include_inf) + let inf_overlay = if include_inf { + renderer.to_inf_overlay() } else { Vec::new() }; diff --git a/apps/halidoscope/src-tauri/src/render.rs b/apps/halidoscope/src-tauri/src/render.rs index d5f9f3d4f8cb..535e8b902e2d 100644 --- a/apps/halidoscope/src-tauri/src/render.rs +++ b/apps/halidoscope/src-tauri/src/render.rs @@ -14,81 +14,41 @@ pub enum NormalizationMode { PerFunc, } -#[derive(Deserialize, Clone, Copy)] -pub struct NanState { - pub active: bool, - pub r: u8, - pub g: u8, - pub b: u8, - pub a: f64, -} - -#[derive(Deserialize, Clone, Copy)] -pub struct InfState { - pub active: bool, - pub r: u8, - pub g: u8, - pub b: u8, - pub a: f64, -} - // A trait that all 2D Canvas renderers implement. pub trait Renderer: Sized { type Value; fn seek(&mut self, trace: &Trace, store_indices: &[usize], target_k: usize); fn to_rgba(&self, normalization_mode: NormalizationMode) -> Vec; - fn to_nan_overlay(&self, nan_state: NanState) -> Vec; - fn to_inf_overlay(&self, inf_state: InfState) -> Vec; + fn to_nan_overlay(&self) -> Vec; + fn to_inf_overlay(&self) -> Vec; fn to_values(&self) -> Vec; } -fn nan_overlay( - values: &[f64], - width: usize, - height: usize, - channels: usize, - nan_state: NanState, -) -> Vec { - let overlay_len = width * height * 4; - let mut out = vec![0u8; overlay_len]; - - let alpha = (nan_state.a * 255.0).clamp(0.0, 255.0) as u8; - - for (chunk, src) in out.chunks_exact_mut(4).zip(values.chunks_exact(channels)) { - if src.iter().any(|v| v.is_nan()) { - chunk[0] = nan_state.r; - chunk[1] = nan_state.g; - chunk[2] = nan_state.b; - chunk[3] = alpha; +/// Packs a per-pixel predicate over channel values into a 1-bit-per-pixel mask: bit 1 for a pixel +/// where `is_set` holds for any channel, bit 0 otherwise. Bits are packed MSB-first within each +/// byte, in the same row-major order as `values`; the final byte is zero-padded if the pixel count +/// isn't a multiple of 8. The frontend expands this back into a colored RGBA8 overlay, since every +/// set pixel shares the same user-selected overlay color. +fn pack_mask(values: &[f64], channels: usize, is_set: impl Fn(&[f64]) -> bool) -> Vec { + let num_pixels = values.len() / channels; + let mut out = vec![0u8; num_pixels.div_ceil(8)]; + + for (i, src) in values.chunks_exact(channels).enumerate() { + if is_set(src) { + out[i / 8] |= 1 << (7 - (i % 8)); } } out } -fn inf_overlay( - values: &[f64], - width: usize, - height: usize, - channels: usize, - inf_state: InfState, -) -> Vec { - let overlay_len = width * height * 4; - let mut out = vec![0u8; overlay_len]; - - let alpha = (inf_state.a * 255.0).clamp(0.0, 255.0) as u8; - - for (chunk, src) in out.chunks_exact_mut(4).zip(values.chunks_exact(channels)) { - if src.iter().any(|v| v.is_infinite()) { - chunk[0] = inf_state.r; - chunk[1] = inf_state.g; - chunk[2] = inf_state.b; - chunk[3] = alpha; - } - } +fn nan_mask(values: &[f64], channels: usize) -> Vec { + pack_mask(values, channels, |src| src.iter().any(|v| v.is_nan())) +} - out +fn inf_mask(values: &[f64], channels: usize) -> Vec { + pack_mask(values, channels, |src| src.iter().any(|v| v.is_infinite())) } // ── Grayscale rendering ────────────────────────────────────────────────────────────────────────── @@ -234,26 +194,12 @@ impl Renderer for GrayscaleState { self.values.clone() } - fn to_nan_overlay(&self, nan_state: NanState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - nan_overlay(&self.values, width, height, channels, nan_state) + fn to_nan_overlay(&self) -> Vec { + nan_mask(&self.values, self.geom.channels) } - fn to_inf_overlay(&self, inf_state: InfState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - inf_overlay(&self.values, width, height, channels, inf_state) + fn to_inf_overlay(&self) -> Vec { + inf_mask(&self.values, self.geom.channels) } } @@ -405,26 +351,12 @@ impl Renderer for RgbState { self.values.clone() } - fn to_nan_overlay(&self, nan_state: NanState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - nan_overlay(&self.values, width, height, channels, nan_state) + fn to_nan_overlay(&self) -> Vec { + nan_mask(&self.values, self.geom.channels) } - fn to_inf_overlay(&self, inf_state: InfState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - inf_overlay(&self.values, width, height, channels, inf_state) + fn to_inf_overlay(&self) -> Vec { + inf_mask(&self.values, self.geom.channels) } } @@ -567,26 +499,12 @@ impl Renderer for StoreFrequencyState { self.counts.clone() } - fn to_nan_overlay(&self, nan_state: NanState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - nan_overlay(&self.values, width, height, channels, nan_state) + fn to_nan_overlay(&self) -> Vec { + nan_mask(&self.values, self.geom.channels) } - fn to_inf_overlay(&self, inf_state: InfState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - inf_overlay(&self.values, width, height, channels, inf_state) + fn to_inf_overlay(&self) -> Vec { + inf_mask(&self.values, self.geom.channels) } } @@ -728,26 +646,12 @@ impl Renderer for LoadFrequencyState { self.counts.clone() } - fn to_nan_overlay(&self, nan_state: NanState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - nan_overlay(&self.values, width, height, channels, nan_state) + fn to_nan_overlay(&self) -> Vec { + nan_mask(&self.values, self.geom.channels) } - fn to_inf_overlay(&self, inf_state: InfState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - inf_overlay(&self.values, width, height, channels, inf_state) + fn to_inf_overlay(&self) -> Vec { + inf_mask(&self.values, self.geom.channels) } } @@ -960,38 +864,24 @@ impl RedundantState { self.redundant_store_counts.clone() } - pub fn to_nan_overlay(&self, nan_state: NanState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - + pub fn to_nan_overlay(&self) -> Vec { let values: Vec = self .last_values .iter() .map(|v| v.map_or(0.0, f64::from_bits)) .collect(); - nan_overlay(&values, width, height, channels, nan_state) + nan_mask(&values, self.geom.channels) } - pub fn to_inf_overlay(&self, inf_state: InfState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - + pub fn to_inf_overlay(&self) -> Vec { let values: Vec = self .last_values .iter() .map(|v| v.map_or(0.0, f64::from_bits)) .collect(); - inf_overlay(&values, width, height, channels, inf_state) + inf_mask(&values, self.geom.channels) } } @@ -1218,26 +1108,12 @@ impl ReuseDistanceState { self.max_reuse_distance.clone() } - pub fn to_nan_overlay(&self, nan_state: NanState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - nan_overlay(&self.values, width, height, channels, nan_state) + pub fn to_nan_overlay(&self) -> Vec { + nan_mask(&self.values, self.geom.channels) } - pub fn to_inf_overlay(&self, inf_state: InfState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - inf_overlay(&self.values, width, height, channels, inf_state) + pub fn to_inf_overlay(&self) -> Vec { + inf_mask(&self.values, self.geom.channels) } } @@ -1459,25 +1335,11 @@ impl ThreadState { (&self.store_counts, &self.load_counts) } - pub fn to_nan_overlay(&self, nan_state: NanState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - nan_overlay(&self.values, width, height, channels, nan_state) + pub fn to_nan_overlay(&self) -> Vec { + nan_mask(&self.values, self.geom.channels) } - pub fn to_inf_overlay(&self, inf_state: InfState) -> Vec { - let FuncGeometry { - width, - height, - channels, - .. - } = self.geom; - - inf_overlay(&self.values, width, height, channels, inf_state) + pub fn to_inf_overlay(&self) -> Vec { + inf_mask(&self.values, self.geom.channels) } } diff --git a/apps/halidoscope/src/utils/api.ts b/apps/halidoscope/src/utils/api.ts index 33d956a3f9cf..901b958c6ac3 100644 --- a/apps/halidoscope/src/utils/api.ts +++ b/apps/halidoscope/src/utils/api.ts @@ -72,17 +72,58 @@ export interface RenderFuncParams { }; } +/** + * Expands a 1-bit-per-pixel row-major overlay mask (MSB-first within each + * byte, as packed by `pack_mask` in `render.rs`) into a dense RGBA8 buffer, + * painting set pixels with `color` and leaving unset pixels fully transparent. + * + * @param mask The packed bitmask, `ceil(width * height / 8)` bytes. + * @param width The width of the buffer the mask covers. + * @param height The height of the buffer the mask covers. + * @param color The color to paint set pixels with. + * @returns A `width * height * 4`-byte RGBA8 buffer. + */ +function expandMask( + mask: Uint8Array, + width: number, + height: number, + color: { + r: number; + g: number; + b: number; + a: number; + }, +): Uint8ClampedArray { + const out = new Uint8ClampedArray(width * height * 4); + const alpha = Math.round(Math.min(1, Math.max(0, color.a)) * 255); + + for (let i = 0; i < width * height; i++) { + const bit = (mask[i >> 3] >> (7 - (i & 7))) & 1; + + if (bit) { + const o = i * 4; + out[o] = color.r; + out[o + 1] = color.g; + out[o + 2] = color.b; + out[o + 3] = alpha; + } + } + + return out; +} + /** * Splits the ArrayBuffer returned by a render command into the tensor data, - * NaN/Inf overlays, and the (optionally returned) tabular data. + * NaN/Inf overlays, and the (optionally returned) tabular data. The NaN/Inf + * overlay planes are 1-bit-per-pixel masks (see `pack_mask` in `render.rs`) + * expanded here into RGBA8 using the same color the caller requested them in, + * since every set pixel in a given overlay shares that one color. * * @param buffer The buffer containing tensor, tabular, and overlay data. * @param width The width of the buffer. * @param height The height of the buffer. - * @param includeNan A flag indicating whether or not to expect a NaN overlay - * in the buffer payload. - * @param includeInf A flag indicating whether or not to expect an Inf overlay - * in the buffer payload. + * @param includeNan The NaN overlay color, or `null` if not requested. + * @param includeInf The Inf overlay color, or `null` if not requested. * @param includeTabularData A flag indicating whether or not to expect tabular * data in the buffer payload. * @returns A {@link RenderFuncResponse}. @@ -98,37 +139,40 @@ function splitRenderBuffer({ buffer: ArrayBuffer; width: number; height: number; - includeNan: boolean; - includeInf: boolean; + includeNan: RenderFuncParams["includeNan"] | null; + includeInf: RenderFuncParams["includeInf"] | null; includeTabularData: boolean; }): RenderFuncResponse { const pixelByteLength = width * height * 4; - const overlayPlaneByteLength = width * height * 4; + const maskByteLength = Math.ceil((width * height) / 8); const overlayBytes = - (includeNan ? overlayPlaneByteLength : 0) + - (includeInf ? overlayPlaneByteLength : 0); + (includeNan ? maskByteLength : 0) + (includeInf ? maskByteLength : 0); const tabularByteLength = buffer.byteLength - pixelByteLength - overlayBytes; + const nanMaskOffset = pixelByteLength; + const infMaskOffset = nanMaskOffset + (includeNan ? maskByteLength : 0); + const tabularOffset = infMaskOffset + (includeInf ? maskByteLength : 0); + return { tensorData: new Uint8ClampedArray(buffer, 0, pixelByteLength), nanOverlayData: includeNan - ? new Uint8ClampedArray(buffer, pixelByteLength, overlayPlaneByteLength) + ? expandMask( + new Uint8Array(buffer, nanMaskOffset, maskByteLength), + width, + height, + includeNan, + ) : null, infOverlayData: includeInf - ? new Uint8ClampedArray( - buffer, - pixelByteLength + (includeNan ? overlayPlaneByteLength : 0), - overlayPlaneByteLength, + ? expandMask( + new Uint8Array(buffer, infMaskOffset, maskByteLength), + width, + height, + includeInf, ) : null, tabularData: includeTabularData - ? new Uint32Array( - buffer, - pixelByteLength + - (includeNan ? overlayPlaneByteLength : 0) + - (includeInf ? overlayPlaneByteLength : 0), - tabularByteLength / 4, - ) + ? new Uint32Array(buffer, tabularOffset, tabularByteLength / 4) : null, }; } @@ -155,16 +199,16 @@ export async function renderGrayscale({ globalIndex, normalizationMode, includeTabularData, - includeNan, - includeInf, + includeNan: includeNan.active, + includeInf: includeInf.active, }); return splitRenderBuffer({ buffer, width, height, - includeNan: includeNan.active, - includeInf: includeInf.active, + includeNan: includeNan.active ? includeNan : null, + includeInf: includeInf.active ? includeInf : null, includeTabularData, }); } @@ -192,16 +236,16 @@ export async function renderRgb({ globalIndex, normalizationMode, includeTabularData, - includeNan, - includeInf, + includeNan: includeNan.active, + includeInf: includeInf.active, }); return splitRenderBuffer({ buffer, width, height, - includeNan: includeNan.active, - includeInf: includeInf.active, + includeNan: includeNan.active ? includeNan : null, + includeInf: includeInf.active ? includeInf : null, includeTabularData, }); } @@ -228,16 +272,16 @@ export async function renderStoreFrequency({ globalIndex, normalizationMode, includeTabularData, - includeNan, - includeInf, + includeNan: includeNan.active, + includeInf: includeInf.active, }); return splitRenderBuffer({ buffer, width, height, - includeNan: includeNan.active, - includeInf: includeInf.active, + includeNan: includeNan.active ? includeNan : null, + includeInf: includeInf.active ? includeInf : null, includeTabularData, }); } @@ -264,16 +308,16 @@ export async function renderLoadFrequency({ globalIndex, normalizationMode, includeTabularData, - includeNan, - includeInf, + includeNan: includeNan.active, + includeInf: includeInf.active, }); return splitRenderBuffer({ buffer, width, height, - includeNan: includeNan.active, - includeInf: includeInf.active, + includeNan: includeNan.active ? includeNan : null, + includeInf: includeInf.active ? includeInf : null, includeTabularData, }); } @@ -306,16 +350,16 @@ export async function renderRedundantStores({ globalIndex, normalizationMode, includeTabularData, - includeNan, - includeInf, + includeNan: includeNan.active, + includeInf: includeInf.active, }); return splitRenderBuffer({ buffer, width, height, - includeNan: includeNan.active, - includeInf: includeInf.active, + includeNan: includeNan.active ? includeNan : null, + includeInf: includeInf.active ? includeInf : null, includeTabularData, }); } @@ -349,16 +393,16 @@ export async function renderReuseDistance({ globalIndex, normalizationMode, includeTabularData, - includeNan, - includeInf, + includeNan: includeNan.active, + includeInf: includeInf.active, }); return splitRenderBuffer({ buffer, width, height, - includeNan: includeNan.active, - includeInf: includeInf.active, + includeNan: includeNan.active ? includeNan : null, + includeInf: includeInf.active ? includeInf : null, includeTabularData, }); } @@ -398,17 +442,17 @@ export async function renderThread({ globalIndex, normalizationMode, opMode: threadOpMode, - threadId: threadId, - includeNan, - includeInf, + threadId, + includeNan: includeNan.active, + includeInf: includeInf.active, }); return splitRenderBuffer({ buffer, width, height, - includeNan: includeNan.active, - includeInf: includeInf.active, + includeNan: includeNan.active ? includeNan : null, + includeInf: includeInf.active ? includeInf : null, includeTabularData, }); } From a266317955f9e2f871f44f984e56f8067fed8074 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Mon, 10 Aug 2026 21:57:01 -0700 Subject: [PATCH 49/67] Fix caching scope bug in FuncNode. --- .../src/components/canvas/FuncNode.tsx | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/apps/halidoscope/src/components/canvas/FuncNode.tsx b/apps/halidoscope/src/components/canvas/FuncNode.tsx index 0f9321a3c775..77fb42ddb761 100644 --- a/apps/halidoscope/src/components/canvas/FuncNode.tsx +++ b/apps/halidoscope/src/components/canvas/FuncNode.tsx @@ -100,9 +100,21 @@ function FuncNode({ data }: NodeProps>) { // Track the playhead position as a ref to avoid re-rendering on every scrub. const latestIndexRef = React.useRef(packetIndex); + // Track whether an active render is in progress. const renderingRef = React.useRef(false); + // Cache render responses if we are outside of a Func's liveness range. + // In addition, add a useEffect call to invalidate the cache if any + // application state affecting the RenderFuncResponse changes. + const cachedPreLiveResultRef = React.useRef(null); + const cachedPostLiveResultRef = React.useRef(null); + + React.useEffect(() => { + cachedPreLiveResultRef.current = null; + cachedPostLiveResultRef.current = null; + }, [render, nan, inf, thread, active]); + React.useEffect(() => { latestIndexRef.current = packetIndex; @@ -120,10 +132,6 @@ function FuncNode({ data }: NodeProps>) { // point — treat those as always live instead of "dead" past index 0. const isRealized = max_store_count > 0; - // Reuse cached buffers for a Func outside of its liveness range. - let cachedPreLiveResult: RenderFuncResponse | null = null; - let cachedPostLiveResult: RenderFuncResponse | null = null; - while (true) { const target = latestIndexRef.current; const notYetLive = target < buffer_liveness.start; @@ -131,10 +139,10 @@ function FuncNode({ data }: NodeProps>) { let result: RenderFuncResponse; - if (notYetLive && cachedPreLiveResult) { - result = cachedPreLiveResult; - } else if (noLongerLive && cachedPostLiveResult) { - result = cachedPostLiveResult; + if (notYetLive && cachedPreLiveResultRef.current) { + result = cachedPreLiveResultRef.current; + } else if (noLongerLive && cachedPostLiveResultRef.current) { + result = cachedPostLiveResultRef.current; } else { const params: RenderFuncParams = { func: name, @@ -181,11 +189,11 @@ function FuncNode({ data }: NodeProps>) { break; } - // Set cached versions if we fall outside a Funcs buffer range. + // Cache the fetch if we fall outside the Func's buffer liveness range. if (notYetLive) { - cachedPreLiveResult = result; + cachedPreLiveResultRef.current = result; } else if (noLongerLive) { - cachedPostLiveResult = result; + cachedPostLiveResultRef.current = result; } } From f42366f9f75b4c56c53affc1a4f3d8ace0e8143b Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Tue, 11 Aug 2026 12:08:54 -0700 Subject: [PATCH 50/67] Use bindgen to derive halide_trace_packet_t's field names and offsets from HalideRuntime.h. Co-authored-by: Claude Opus 5 --- apps/halidoscope/src-tauri/Cargo.lock | 92 ++++++++++++++++++++++++- apps/halidoscope/src-tauri/Cargo.toml | 1 + apps/halidoscope/src-tauri/build.rs | 43 +++++++++++- apps/halidoscope/src-tauri/src/trace.rs | 72 ++++++++++--------- 4 files changed, 170 insertions(+), 38 deletions(-) diff --git a/apps/halidoscope/src-tauri/Cargo.lock b/apps/halidoscope/src-tauri/Cargo.lock index 935010640855..61a4ba92f546 100644 --- a/apps/halidoscope/src-tauri/Cargo.lock +++ b/apps/halidoscope/src-tauri/Cargo.lock @@ -284,6 +284,26 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.12.1", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.117", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -482,7 +502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -491,6 +511,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfb" version = "0.7.3" @@ -530,6 +559,17 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + [[package]] name = "clap" version = "4.6.1" @@ -982,6 +1022,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + [[package]] name = "embed-resource" version = "3.0.9" @@ -1559,6 +1605,7 @@ dependencies = [ name = "halidoscope" version = "0.1.0" dependencies = [ + "bindgen", "colorous", "comfy-table", "palette", @@ -1920,6 +1967,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2064,7 +2120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -2093,6 +2149,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libredox" version = "0.1.17" @@ -2167,6 +2233,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2239,6 +2311,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -3342,6 +3424,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" diff --git a/apps/halidoscope/src-tauri/Cargo.toml b/apps/halidoscope/src-tauri/Cargo.toml index 2456437574ee..fa4d693220cc 100644 --- a/apps/halidoscope/src-tauri/Cargo.toml +++ b/apps/halidoscope/src-tauri/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } +bindgen = "0.72" [dependencies] tauri = { version = "2", features = [] } diff --git a/apps/halidoscope/src-tauri/build.rs b/apps/halidoscope/src-tauri/build.rs index d860e1e6a7ca..f1adf986bbe3 100644 --- a/apps/halidoscope/src-tauri/build.rs +++ b/apps/halidoscope/src-tauri/build.rs @@ -1,3 +1,42 @@ -fn main() { - tauri_build::build() +use std::env; +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + tauri_build::build(); + generate_trace_packet_bindings() +} + +/// Derives Rust type layouts for `halide_trace_packet_t` (and, transitively, the +/// `halide_trace_event_code_t` enum it references) directly from Halide's runtime header, so the +/// wire-format struct used by `trace.rs` stays mechanically in sync with the real C++ definition +/// instead of being hand-copied. +/// +/// This only derives *layout*: it deliberately does not generate bindings for any of +/// `halide_trace_packet_t`'s C++ methods (e.g., `coordinates()`, `value()`, `func()`). +fn generate_trace_packet_bindings() -> Result<(), Box> { + let manifest_dir = env::var("CARGO_MANIFEST_DIR")?; + let header_path = PathBuf::from(&manifest_dir).join("../../../src/runtime/HalideRuntime.h"); + let header = header_path.canonicalize().map_err(|e| { + format!( + "HalideRuntime.h not found at {}: {e}", + header_path.display() + ) + })?; + + // Flag to cargo that we only need to rerun bindgen if HalideRuntime.h's contents have changed. + println!("cargo:rerun-if-changed={}", header.display()); + + let bindings = bindgen::Builder::default() + .header(header.to_string_lossy()) + .clang_args(["-x", "c++", "-std=c++17"]) + .allowlist_type("halide_trace_packet_t") + .with_codegen_config(bindgen::CodegenConfig::TYPES | bindgen::CodegenConfig::VARS) + .derive_default(true) + .generate()?; + + // Write generated bindings to this build's hashed OUT_DIR (as opposed to committing to source). + let out_path = PathBuf::from(env::var("OUT_DIR")?); + bindings.write_to_file(out_path.join("halide_trace_bindings.rs"))?; + + Ok(()) } diff --git a/apps/halidoscope/src-tauri/src/trace.rs b/apps/halidoscope/src-tauri/src/trace.rs index 7a98867a7e60..e625c9a5c65c 100644 --- a/apps/halidoscope/src-tauri/src/trace.rs +++ b/apps/halidoscope/src-tauri/src/trace.rs @@ -224,39 +224,33 @@ pub struct Trace { // ── Binary parsing helpers ─────────────────────────────────────────────────────────────────────── -// halide_trace_packet_t fixed header: 6 × 4 bytes = 24 bytes. -// u32 size @ 0 -// i32 event @ 4 -// i32 parent_id @ 8 -// union { i32 id; i32 value_index; } @ 12 -// union { type{code, bits, lanes}; i32 thread_id; } @ 16 -// i32 dimensions @ 20 +// Layout for `halide_trace_packet_t`'s fixed header, derived directly from HalideRuntime.h by +// bindgen (see build.rs) rather than hand-copied, so a layout change upstream is caught at compile +// time instead of silently desyncing. This only derives the struct's data layout: none of +// `halide_trace_packet_t`'s C++ accessor methods are bound, so the variable-length trailing data +// below is still walked by hand. // // Immediately after the header: // i32 coordinates[dimensions] // u8 value[type.lanes * ceil(type.bits / 8)] // char func[] (null-terminated) // char trace_tag[] (null-terminated; empty string if absent) -const HEADER_BYTES: usize = 24; - -// Helper functions to read little-endian integers from a byte buffer at a given offset. try_into() -// will convert the slice to a fixed-size [u8; N] array. We inline these for performance since they -// are called in the hot path of packet parsing. -#[inline] -fn u32_le(buf: &[u8], off: usize) -> u32 { - u32::from_le_bytes(buf[off..off + 4].try_into().unwrap()) +mod ffi { + #![allow(non_camel_case_types, non_upper_case_globals, dead_code)] + include!(concat!(env!("OUT_DIR"), "/halide_trace_bindings.rs")); } +use ffi::halide_trace_packet_t; + +const HEADER_BYTES: usize = std::mem::size_of::(); +// Helper function to read a little-endian i32 out of a byte buffer at a given offset. try_into() +// converts the slice to a fixed-size [u8; 4] array. Inlined for performance since it's called in +// the hot path of packet parsing (coordinate arrays). #[inline] fn i32_le(buf: &[u8], off: usize) -> i32 { i32::from_le_bytes(buf[off..off + 4].try_into().unwrap()) } -#[inline] -fn u16_le(buf: &[u8], off: usize) -> u16 { - u16::from_le_bytes(buf[off..off + 2].try_into().unwrap()) -} - /// Read a null-terminated C string. Returns `(string, bytes_consumed_including_null)`. fn read_cstr(buf: &[u8]) -> (&str, usize) { let null_idx = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); @@ -448,33 +442,43 @@ impl Trace { // Packet parsing loop. while pos + HEADER_BYTES <= total { - let size = u32_le(data, pos) as usize; + // Don't reinterpret-cast `data`'s bytes directly as `&halide_trace_packet_t` — we + // cannot statically prove the buffer is 4-byte aligned (though it practice it always + // should be), and an unaligned reference is UB. `read_unaligned` copies the header out + // by value instead. + let header: halide_trace_packet_t = + unsafe { std::ptr::read_unaligned(data[pos..pos + HEADER_BYTES].as_ptr().cast()) }; + + let size = header.size as usize; if size < HEADER_BYTES || pos + size > total { break; } // ── Fixed header fields ─────────────────────────────────────────── - let event = i32_le(data, pos + 4); - let parent_id = i32_le(data, pos + 8); - let dimensions = i32_le(data, pos + 20) as usize; + let parent_id = header.parent_id; + let dimensions = header.dimensions as usize; - let ev = EventCode::from_i32(event); + let ev = EventCode::from_i32(header.event as i32); let is_load_or_store = matches!(ev, EventCode::Load | EventCode::Store); - // Slot @ 12 is `id` for non-load/store events and `value_index` for load/store - // events; slot @ 16 is `type` for load/store events and `thread_id` (else 0) - // otherwise. See halide_trace_packet_t in HalideRuntime.h. + // `__bindgen_anon_1` is `id` for non-load/store events and `value_index` for + // load/store events; `__bindgen_anon_2` is `type` for load/store events and + // `thread_id` (else 0) otherwise. See halide_trace_packet_t in HalideRuntime.h. + // Reading a union field is inherently unsafe — the type can't track which variant + // is valid, so it's on us to pick the right one based on `ev`, same as the raw + // offset reads this replaced. let (id, value_index) = if is_load_or_store { - (0, i32_le(data, pos + 12)) + (0, unsafe { header.__bindgen_anon_1.value_index }) } else { - (i32_le(data, pos + 12), 0) + (unsafe { header.__bindgen_anon_1.id }, 0) }; let (type_, thread_id) = if is_load_or_store { + let inner = unsafe { header.__bindgen_anon_2.__bindgen_anon_1 }; let type_ = HalideType { - code: TypeCode::from_u8(data[pos + 16]), - bits: data[pos + 17], - lanes: u16_le(data, pos + 18), + code: TypeCode::from_u8(inner.type_code), + bits: inner.type_bits, + lanes: inner.lanes, }; // Initialize thread_ids as a sentinel value of -1. @@ -486,7 +490,7 @@ impl Trace { bits: 0, lanes: 0, }, - i32_le(data, pos + 16), + unsafe { header.__bindgen_anon_2.thread_id }, ) }; From 1f5cdabb2a16e30c518c7b79f6491e421fe7a720 Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Thu, 13 Aug 2026 13:19:34 -0700 Subject: [PATCH 51/67] Introduce RAII type to support accumulating profiler stats in Halidoscope's JIT profiling runs. Co-authored-by: Claude Opus 5 --- .../src/halide/halide_/PyPipeline.cpp | 6 +- src/Pipeline.cpp | 62 +++++++++++++++++-- src/Pipeline.h | 13 ++++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/python_bindings/src/halide/halide_/PyPipeline.cpp b/python_bindings/src/halide/halide_/PyPipeline.cpp index 70589163f6e0..6b1229583886 100644 --- a/python_bindings/src/halide/halide_/PyPipeline.cpp +++ b/python_bindings/src/halide/halide_/PyPipeline.cpp @@ -67,8 +67,12 @@ void define_pipeline(py::module &m) { py::class_(m, "HalidoscopeOptions") .def(py::init<>()) .def_readwrite("halidoscope_path", &HalidoscopeOptions::halidoscope_path) + .def_readwrite("halidoscope_output_dir", &HalidoscopeOptions::halidoscope_output_dir) + .def_readwrite("halidoscope_profile_runs", &HalidoscopeOptions::halidoscope_profile_runs) .def("__repr__", [](const HalidoscopeOptions &o) -> std::string { - return ""; + return ""; }); auto pipeline_class = diff --git a/src/Pipeline.cpp b/src/Pipeline.cpp index 0470aa789aa4..ec0218605217 100644 --- a/src/Pipeline.cpp +++ b/src/Pipeline.cpp @@ -158,6 +158,8 @@ struct PipelineContents { bool trace_pipeline = false; + bool defer_profile_flush = false; + /** Optional prefixes used to rename halide_-prefixed runtime symbols. * Empty unless set via Pipeline::apply_runtime_prefixes(). */ RuntimePrefixParams runtime_prefixes_params; @@ -844,7 +846,11 @@ Realization Pipeline::realize(JITUserContext *context, } // If we're profiling, report runtimes and reset profiler stats. - contents->jit_cache.finish_profiling(context); + // Condition based on whether or not calling code has chosen to defer + // flushing profile information (e.g., to accumulate results across runs). + if (!contents->defer_profile_flush) { + contents->jit_cache.finish_profiling(context); + } jit_context.finalize(exit_status); // Crop back to the requested size if necessary @@ -907,6 +913,17 @@ void Pipeline::trace_pipeline() { contents->trace_pipeline = true; } +void Pipeline::set_defer_profile_flush(bool defer) { + user_assert(defined()) << "Pipeline is undefined\n"; + contents->defer_profile_flush = defer; +} + +void Pipeline::flush_profiler_state(JITUserContext *context) { + user_assert(defined()) << "Pipeline is undefined\n"; + JITUserContext empty{}; + contents->jit_cache.finish_profiling(context ? context : &empty); +} + namespace { // State for the custom_trace callbacks below. custom_trace is a plain C @@ -1079,6 +1096,9 @@ void Pipeline::halidoscope_impl(const std::function= 0) + << "halidoscope: HalidoscopeOptions::halidoscope_profile_runs must be a non-negative integer, got " + << options.halidoscope_profile_runs << ".\n"; // Fail fast if halidoscope_path looks like an explicit path (as opposed // to a bare name meant to be resolved via $PATH, e.g. the default @@ -1134,7 +1154,18 @@ void Pipeline::halidoscope_impl(const std::function 0) { Pipeline profiled = deserialize_pipeline(data, external_params); profiled.trace_pipeline(); Target profile_target = base_target.with_feature(Target::Profile); @@ -1144,7 +1175,12 @@ void Pipeline::halidoscope_impl(const std::function halidoscope_args = {binary, "--trace", trace_path}; + if (options.halidoscope_profile_runs > 0) { + halidoscope_args.push_back("--profile"); + halidoscope_args.push_back(profile_path); + } + + int halidoscope_rc = run_process(halidoscope_args); // If we did not specify a persistent output directory, clean up the // temporary directory storing trace and profile data. if (!options.halidoscope_output_dir) { file_unlink(trace_path); - file_unlink(profile_path); + if (options.halidoscope_profile_runs > 0) { + file_unlink(profile_path); + } dir_rmdir(dir); } @@ -1397,7 +1442,12 @@ void Pipeline::realize(JITUserContext *context, debug(2) << "Back from jitted function. Exit status was " << exit_status << "\n"; // If we're profiling, report runtimes and reset profiler stats. - contents->jit_cache.finish_profiling(context); + // Condition based on whether or not calling code has chosen to defer + // flushing profile information (e.g., to accumulate results across + // many runs). + if (!contents->defer_profile_flush) { + contents->jit_cache.finish_profiling(context); + } jit_call_context.finalize(exit_status); } diff --git a/src/Pipeline.h b/src/Pipeline.h index b58cf4b67784..f62de10f90b0 100644 --- a/src/Pipeline.h +++ b/src/Pipeline.h @@ -123,6 +123,9 @@ struct HalidoscopeOptions { /** (Optional) Path to the non-volatile directory for storing * Halidoscope-generated trace binaries and profiler output. */ std::optional halidoscope_output_dir = std::nullopt; + /** The number of runs for the profiler execution. Defaults to 1. A + * 0 value indicates that profiling should be skipped. */ + int halidoscope_profile_runs = 1; }; class Pipeline; @@ -547,6 +550,16 @@ class Pipeline { void halidoscope(RealizationArg output, HalidoscopeOptions options = HalidoscopeOptions(), const Target &target = Target()); // @} + /** Set a flag to defer automatically flushing profiler state when calling + * Pipeline::realize, which automatically calls jit_cache.finish_profiling() + * after each realization in a JIT pipeline. Useful to accumulate metrics + * from multiple profiling runs. */ + void set_defer_profile_flush(bool defer); + + /** Immediately flush profiler state, using the profiler's built-in state + * accumulation logic. */ + void flush_profiler_state(JITUserContext *context = nullptr); + private: std::string generate_function_name() const; From b8456296f5abcc9d953418cbf033f6992686045e Mon Sep 17 00:00:00 2001 From: Parker Ziegler Date: Sun, 16 Aug 2026 16:41:04 -0700 Subject: [PATCH 52/67] Standardize styles for Select and Checkbox components and reuse across UI. Co-authored-by: Claude Sonnet 5 --- .../controls/graph/GraphDisplay.tsx | 21 +- .../components/controls/inf/InfControls.tsx | 78 ++--- .../controls/liveness/LivenessControls.tsx | 24 +- .../components/controls/nan/NaNControls.tsx | 78 ++--- .../components/controls/render/RenderMode.tsx | 39 +-- .../controls/render/RenderModeParameters.tsx | 305 ++++-------------- .../src/components/icons/ArrowDownIcon.tsx | 20 -- .../src/components/icons/CheckIcon.tsx | 21 -- .../src/components/shared/Checkbox.tsx | 44 +++ .../src/components/shared/Select.tsx | 98 ++++++ 10 files changed, 272 insertions(+), 456 deletions(-) delete mode 100644 apps/halidoscope/src/components/icons/ArrowDownIcon.tsx delete mode 100644 apps/halidoscope/src/components/icons/CheckIcon.tsx create mode 100644 apps/halidoscope/src/components/shared/Checkbox.tsx create mode 100644 apps/halidoscope/src/components/shared/Select.tsx diff --git a/apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx b/apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx index 1a445f04d7f6..51f3851d0e17 100644 --- a/apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx +++ b/apps/halidoscope/src/components/controls/graph/GraphDisplay.tsx @@ -1,9 +1,8 @@ import { type Edge } from "@xyflow/react"; import { useSetAtom } from "jotai"; -import { Checkbox } from "radix-ui"; import * as React from "react"; -import CheckIcon from "@/components/icons/CheckIcon"; +import Checkbox from "@/components/shared/Checkbox"; import { edgesAtom } from "@/state/graph"; function hideEdge(hidden: boolean) { @@ -25,18 +24,12 @@ function GraphDisplay() { } return ( -
- - - - - - -
+ ); } diff --git a/apps/halidoscope/src/components/controls/inf/InfControls.tsx b/apps/halidoscope/src/components/controls/inf/InfControls.tsx index 00200807a03d..98bc38005151 100644 --- a/apps/halidoscope/src/components/controls/inf/InfControls.tsx +++ b/apps/halidoscope/src/components/controls/inf/InfControls.tsx @@ -1,10 +1,9 @@ import * as d3 from "d3"; import { useAtom } from "jotai"; -import { Checkbox, Label, Select } from "radix-ui"; import ColorInput from "@/components/controls/color/ColorInput"; -import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; -import CheckIcon from "@/components/icons/CheckIcon"; +import Checkbox from "@/components/shared/Checkbox"; +import Select from "@/components/shared/Select"; import { DEFAULT_INF_COLOR, infAtom, @@ -17,63 +16,26 @@ function InfControls() { return (
-
- { - setInf({ ...inf, active: !!checked }); - }} - > - - - - - -
+ setInf({ ...inf, active })} + /> {inf.active ? (
-
- - Animation - - - setInf({ ...inf, animationMode: value as AnimationMode }) - } - > - - - - - - - - - {ANIMATION_MODES.map((value) => ( - - {value} - - ))} - - - -
+ + setNan({ ...nan, animationMode: value as AnimationMode }) + } + items={ANIMATION_MODES.map((mode) => ({ + value: mode, + label: mode, + }))} + /> setRender({ ...render, renderMode: value as RenderMode }) } - > - - - - - - - - - {RENDER_MODES.map((value) => ( - - {value} - - ))} - - - + items={RENDER_MODES.map((mode) => ({ value: mode, label: mode }))} + /> ); } -export default VisualizationSelect; +export default RenderMode; diff --git a/apps/halidoscope/src/components/controls/render/RenderModeParameters.tsx b/apps/halidoscope/src/components/controls/render/RenderModeParameters.tsx index 3500eeeb8bbc..a687466f0658 100644 --- a/apps/halidoscope/src/components/controls/render/RenderModeParameters.tsx +++ b/apps/halidoscope/src/components/controls/render/RenderModeParameters.tsx @@ -1,8 +1,7 @@ import { useAtom } from "jotai"; -import { Label, Select } from "radix-ui"; import * as React from "react"; -import ArrowDownIcon from "@/components/icons/ArrowDownIcon"; +import Select from "@/components/shared/Select"; import { useTraceContext } from "@/hooks/trace"; import { funcAtom } from "@/state/func"; import { type NormalizationMode, renderAtom } from "@/state/render"; @@ -27,210 +26,69 @@ function RenderModeParameters() { case "Reuse Distance": return (
-
- - Scale - - - setTabularData({ ...tabularData, scale: value as Scale }) - } - > - - - - - - - - - - Linear - - - Log - - - - -
-
- - Normalize Display - - - setRender({ - ...render, - normalizationMode: value as NormalizationMode, - }) - } - > - - - - - - - - - - Across Funcs - - - Per Func - - - - -
+ + setRender({ + ...render, + normalizationMode: value as NormalizationMode, + }) + } + items={[ + { value: "Across Funcs", label: "Across Funcs" }, + { value: "Per Func", label: "Per Func" }, + ]} + />
); case "Thread Coverage": return (
-
- - Operation - - { - setThread({ ...thread, op: value as "Load" | "Store" }); - }} - > - - - - - - - - - - - - Store - - - Load - - - - -
-
- - Filter by Thread - - { - setThread({ - ...thread, - id: value === "None" ? NO_THREAD_INFO_SENTINEL_ID : value, - }); - }} - > - - - - - - - - - - - - None - - {funcs[activeFunc].thread_ids - .filter((threadId) => threadId !== "0") - .map((threadId) => ( - - {threadId} - - ))} - - - -
+ { + setThread({ + ...thread, + id: value === "None" ? NO_THREAD_INFO_SENTINEL_ID : value, + }); + }} + items={[ + { value: "None", label: "None" }, + ...funcs[activeFunc].thread_ids + .filter((threadId) => threadId !== "0") + .map((threadId) => ({ value: threadId, label: threadId })), + ]} + />
); } @@ -247,41 +105,16 @@ function RenderModeParameters() { return (
-
- - Selected Func - - - - - - - - - - - - - {Object.keys(funcs).map((func) => ( - - {func} - - ))} - - - -
+