From 2b3050c5cc1f9fcf3190a53c10a0a437fd644198 Mon Sep 17 00:00:00 2001 From: Shahmir Varqha Date: Sat, 14 Dec 2024 16:01:19 +0800 Subject: [PATCH] add experimental tracing panel --- .../src/components/editor/cell/CellStatus.tsx | 4 +- .../editor/chrome/panels/tracing-panel.tsx | 7 + .../src/components/editor/chrome/types.ts | 9 + .../src/components/editor/dynamic-favicon.tsx | 2 +- frontend/src/components/tracing/tracing.tsx | 391 ++++++++++++++++++ 5 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/editor/chrome/panels/tracing-panel.tsx create mode 100644 frontend/src/components/tracing/tracing.tsx diff --git a/frontend/src/components/editor/cell/CellStatus.tsx b/frontend/src/components/editor/cell/CellStatus.tsx index ceeee2cb482..b748937d14c 100644 --- a/frontend/src/components/editor/cell/CellStatus.tsx +++ b/frontend/src/components/editor/cell/CellStatus.tsx @@ -321,7 +321,7 @@ export const CellStatusComponent: React.FC = ({ return null; }; -const ElapsedTime = (props: { elapsedTime: string }) => { +export const ElapsedTime = (props: { elapsedTime: string }) => { return ( {props.elapsedTime} ); @@ -346,7 +346,7 @@ const LastRanTime = (props: { lastRanTime: number }) => { ); }; -function formatElapsedTime(elapsedTime: number | null) { +export function formatElapsedTime(elapsedTime: number | null) { if (elapsedTime === null) { return ""; } diff --git a/frontend/src/components/editor/chrome/panels/tracing-panel.tsx b/frontend/src/components/editor/chrome/panels/tracing-panel.tsx new file mode 100644 index 00000000000..a80d3284c98 --- /dev/null +++ b/frontend/src/components/editor/chrome/panels/tracing-panel.tsx @@ -0,0 +1,7 @@ +/* Copyright 2024 Marimo. All rights reserved. */ +import { Tracing } from "@/components/tracing/tracing"; +import React from "react"; + +export const TracingPanel: React.FC = () => { + return ; +}; diff --git a/frontend/src/components/editor/chrome/types.ts b/frontend/src/components/editor/chrome/types.ts index 387e54edda1..42760482b4e 100644 --- a/frontend/src/components/editor/chrome/types.ts +++ b/frontend/src/components/editor/chrome/types.ts @@ -14,6 +14,7 @@ import { NotebookPenIcon, BoxIcon, BotMessageSquareIcon, + ActivityIcon, } from "lucide-react"; export type PanelType = @@ -22,6 +23,7 @@ export type PanelType = | "variables" | "outline" | "dependencies" + | "tracing" | "packages" | "documentation" | "snippets" @@ -94,6 +96,13 @@ export const PANELS: PanelDescriptor[] = [ tooltip: "Notebook logs", position: "sidebar", }, + { + type: "tracing", + Icon: ActivityIcon, + tooltip: "Tracing", + position: "sidebar", + hidden: !getFeatureFlag("tracing"), + }, { type: "snippets", Icon: SquareDashedBottomCodeIcon, diff --git a/frontend/src/components/editor/dynamic-favicon.tsx b/frontend/src/components/editor/dynamic-favicon.tsx index 9bcdfcfea37..572ab716585 100644 --- a/frontend/src/components/editor/dynamic-favicon.tsx +++ b/frontend/src/components/editor/dynamic-favicon.tsx @@ -4,7 +4,7 @@ import { useEventListener } from "@/hooks/useEventListener"; import { usePrevious } from "@dnd-kit/utilities"; import { useEffect } from "react"; -const FAVICONS = { +export const FAVICONS = { idle: "./favicon.ico", success: "./circle-check.ico", running: "./circle-play.ico", diff --git a/frontend/src/components/tracing/tracing.tsx b/frontend/src/components/tracing/tracing.tsx new file mode 100644 index 00000000000..506ffeaebe0 --- /dev/null +++ b/frontend/src/components/tracing/tracing.tsx @@ -0,0 +1,391 @@ +/* Copyright 2024 Marimo. All rights reserved. */ +import { FAVICONS } from "@/components/editor/dynamic-favicon"; +import React, { useRef, useState } from "react"; +import type { CellId } from "@/core/cells/ids"; +import { ElapsedTime, formatElapsedTime } from "../editor/cell/CellStatus"; +import { Tooltip } from "@/components/ui/tooltip"; +import { type Config, type TopLevelSpec, compile } from "vega-lite"; +import { ChevronRight, ChevronDown, SettingsIcon } from "lucide-react"; +import type { VisualizationSpec } from "react-vega"; + +const LazyVegaLite = React.lazy(() => + import("react-vega").then((m) => ({ default: m.VegaLite })), +); + +interface CellRun { + cellID: string; + code: string; + elapsedTime: number; + status: RunStatus; +} + +export interface Run { + runId: string; + cellRuns: CellRun[]; + runStartTime: string; +} + +// assumes the cellRuns are ordered correctly +const mockRuns: Run[] = [ + { + runId: "7", + cellRuns: [ + { + cellID: "1", + code: "import marimo as mo", + elapsedTime: 23_000, + status: "success", + }, + { + cellID: "2", + code: "def generate_some()", + elapsedTime: 46, + status: "success", + }, + { + cellID: "3", + code: "def generate_some()", + elapsedTime: 46, + status: "success", + }, + ], + runStartTime: "Dec 7 2.03pm", + }, + { + runId: "8", + cellRuns: [ + { + cellID: "1", + code: "import marimo as mo", + elapsedTime: 25, + status: "success", + }, + { + cellID: "2", + code: "def generate_some()", + elapsedTime: 100, + status: "success", + }, + { + cellID: "3", + code: "def generate_some()", + elapsedTime: 4600, + status: "error", + }, + { + cellID: "4", + code: "[print(i) for i in range(1, 100)]", + elapsedTime: 8, + status: "error", // should be disabled? + }, + ], + runStartTime: "Dec 7 2.01pm", + }, +]; + +interface Values { + cell: number; + startTimestamp: string; + endTimestamp: string; + elapsedTime: string; +} + +export interface Data { + values: Values[]; +} + +const sampleData: Data = { + values: [ + { + cell: 1, + startTimestamp: "2024-12-01 11:00:00.000", + endTimestamp: "2024-12-01 11:00:00.230", + elapsedTime: "23ms", + }, + { + cell: 2, + startTimestamp: "2024-12-01 11:00:00.230", + endTimestamp: "2024-12-01 11:00:00.466", + elapsedTime: "46ms", + }, + { + cell: 3, + startTimestamp: "2024-12-01 11:00:00.466", + endTimestamp: "2024-12-01 11:00:00.636", + elapsedTime: "46ms", + }, + ], +}; + +const baseSpec: TopLevelSpec = { + $schema: "https://vega.github.io/schema/vega-lite/v5.json", + mark: { + type: "bar", + cornerRadius: 2, + fill: "#37BE5F", // same colour as chrome's network tab + }, + height: { step: 23 }, + params: [ + { + name: "zoomAndPan", + select: "interval", + bind: "scales", + }, + { + name: "hoveredCellID", + bind: { element: "#hiddenInputElement" }, + }, + { + name: "cursor", + value: "grab", + }, + ], + encoding: { + y: { + field: "cell", + axis: null, + scale: { paddingInner: 0.2 }, + }, + x: { + field: "startTimestamp", + type: "temporal", + axis: { orient: "top", title: null }, + }, + x2: { field: "endTimestamp", type: "temporal" }, + tooltip: [ + { field: "cell", title: "Cell" }, + { + field: "startTimestamp", + type: "temporal", + timeUnit: "monthdatehoursminutessecondsmilliseconds", + title: "Start", + }, + { + field: "endTimestamp", + type: "temporal", + timeUnit: "monthdatehoursminutessecondsmilliseconds", + title: "End", + }, + ], + size: { + value: { expr: "hoveredCellID == toString(datum.cell) ? 25 : 20" }, + }, + }, +}; + +function createGanttVegaLiteSpec(data: Data): TopLevelSpec { + return { + ...baseSpec, + data, + }; +} + +const config: Config = { + view: { + stroke: "transparent", + }, +}; + +export const Tracing: React.FC = () => { + // TODO: Either we get the runs of cells from FE side or BE side + + const [chartPosition, setChartPosition] = + useState("sideBySide"); + + const toggleConfig = () => { + if (chartPosition === "above") { + setChartPosition("sideBySide"); + } else { + setChartPosition("above"); + } + }; + + return ( +
+ + + + +
+ {mockRuns.map((run) => ( + + ))} +
+
+ ); +}; + +type ChartPosition = "sideBySide" | "above"; + +interface ChartProps { + className?: string; + width: number; + height: number; + vegaSpec: VisualizationSpec; +} + +const Chart: React.FC = (props: ChartProps) => { + return ( +
+ +
+ ); +}; + +const TraceBlock: React.FC<{ run: Run; chartPosition: ChartPosition }> = ({ + run, + chartPosition, +}) => { + // TODO: Initial one should be false, all the others are true + const [collapsed, setCollapsed] = useState(false); + + // Used to sync Vega charts and React components + // Note that this will only work for the first chart for now, until we create unique input elements + const [hoveredCellID, setHoveredCellID] = useState(); + const hiddenInputRef = useRef(null); + + const hoverOnCell = (cellID: number) => { + setHoveredCellID(cellID); + // dispatch input event to trigger vega's param to update + if (hiddenInputRef.current) { + hiddenInputRef.current.value = String(cellID); + hiddenInputRef.current.dispatchEvent( + new Event("input", { bubbles: true }), + ); + } + }; + + const ChevronComponent = () => { + const Icon = collapsed ? ChevronRight : ChevronDown; + return ; + }; + + const TraceTitle = ( + setCollapsed(!collapsed)} + > + Run - {run.runStartTime} + + + ); + + const vegaSpec = compile(createGanttVegaLiteSpec(sampleData), { + config, + }).spec; + + const TraceRows = ( +
+ + {run.cellRuns.map((cellRun) => ( + + ))} +
+ ); + + if (chartPosition === "above") { + return ( +
+
+          {TraceTitle}
+          {!collapsed && }
+          {!collapsed && TraceRows}
+        
+
+ ); + } + + return ( +
+
+        {TraceTitle}
+        {!collapsed && TraceRows}
+      
+ {!collapsed && ( + + )} +
+ ); +}; + +type RunStatus = "success" | "running" | "error"; + +interface TraceRowProps { + timestamp: string; + cellID: CellId; + code: string; + elapsedTime: number; + status: RunStatus; + hoverOnCell: (cellID: number) => void; +} + +const TraceRow: React.FC = (props: TraceRowProps) => { + const elapsedTimeStr = formatElapsedTime(props.elapsedTime); + const elapsedTimeTooltip = ( + + This cell took to run + + ); + + const handleMouseEnter = () => { + props.hoverOnCell(props.cellID); + }; + + return ( +
+ [{props.timestamp}] + + {/* () */} + (cell-1) + + {props.code} + +
+ + {elapsedTimeStr} + + + {/* TODO: Shouldn't use favicon. */} + + {`${props.status} + +
+
+ ); +};