diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a99f3c0effb..06fd09d9de1 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -84,6 +84,7 @@ RUN curl -fsSL https://claude.ai/install.sh | bash RUN \ rustup install $RUST_VERSION && \ + rustup default $RUST_VERSION && \ rustup component add rustfmt && \ rustup component add clippy && \ cargo install cargo-make diff --git a/js-packages/profiler-app/src/App.svelte b/js-packages/profiler-app/src/App.svelte index d59000790d5..960e370f1bc 100644 --- a/js-packages/profiler-app/src/App.svelte +++ b/js-packages/profiler-app/src/App.svelte @@ -20,7 +20,7 @@ let sqlPanelFullHeight = $state(false) let profileData: { - profile: JsonProfiles + profile: JsonProfiles | undefined dataflow: Dataflow | undefined sources: string[] | undefined logText: string | undefined @@ -65,7 +65,7 @@ const suitableProfiles = getSuitableProfiles(zipData) if (suitableProfiles.length === 0) { throw new Error( - 'No suitable profiles found in the uploaded support bundle. Check if it contains the circuit profile and dataflow graph (optional).' + 'No readable data found in the uploaded support bundle. Check that it contains a circuit profile, config, logs or stats.' ) } profileFiles = suitableProfiles @@ -147,8 +147,8 @@

- Load a Feldera support bundle (.zip file) containing circuit profile data to begin - visualization. + Load a Feldera support bundle (.zip file) to explore the circuit profile, + program code, logs and the pipeline config.

{#if isLoading} diff --git a/js-packages/profiler-layout/src/lib/components/SupportBundleViewerLayout.svelte b/js-packages/profiler-layout/src/lib/components/SupportBundleViewerLayout.svelte index 96d588c24c5..21b6f7ec623 100644 --- a/js-packages/profiler-layout/src/lib/components/SupportBundleViewerLayout.svelte +++ b/js-packages/profiler-layout/src/lib/components/SupportBundleViewerLayout.svelte @@ -39,7 +39,8 @@ type AnalysisTab = (typeof TABS)[number] interface Props { - profileData: JsonProfiles + /** Parsed circuit profile */ + profileData?: JsonProfiles dataflowData: Dataflow | undefined programCode: string[] | undefined logText?: string @@ -81,6 +82,12 @@ onRenderingChange }: Props = $props() + const hasProfile = $derived(profileData !== undefined) + + const graphPaneDefaultSize = 55 + const graphPaneMinSize = 20 + const belowGraphPaneDefaultSize = 100 - graphPaneDefaultSize + let profilerDiagram: ProfilerDiagram | undefined = $state() // The loaded profile's toplevel node id, so the analysis panel can recognise overview data. const diagramRootNodeId = $derived(profilerDiagram?.getProfile()?.rootNodeId) @@ -137,7 +144,6 @@ const attrs = data.match({ some: (v) => v, none: () => null }) untrack(() => { if (attrs) { - currentTab = 'Metrics' tooltipData = { nodeAttributes: attrs } // Only cache while we're on a node view, so an `'overview'` round-trip can't // overwrite the operator the user actually inspected. @@ -205,19 +211,18 @@ profilerDiagram?.selectMetric(selectedMetricId) }) - // Show the overview each time a profile loads. `analysisView` is reset first so the + // Reset to the overview whenever the loaded bundle changes. `analysisView` is reset first so the // SegmentedControl indicator follows the new view, and so the sticky `displayNodeAttributes` // that `showGlobalMetrics` triggers sees a non-node view and can't overwrite `lastNodeData` // with the overview payload. $effect(() => { void profileData const diagram = profilerDiagram - if (!diagram) { - return - } queueMicrotask(() => { analysisView = 'overview' - diagram.showGlobalMetrics(true) + tooltipData = null + lastNodeData = null + diagram?.showGlobalMetrics(true) }) }) @@ -316,42 +321,54 @@ {#snippet graphPanel()} -
+
{@render loadProfileControl?.()} -
- e.key === 'Enter' && handleSearch()} - class="input h-6 w-36 text-sm" - /> -
+ {#if hasProfile} +
+ e.key === 'Enter' && handleSearch()} + class="input h-6 w-36 text-sm" + /> +
+ {:else} +

+ This bundle has no circuit profile, so the dataflow graph is unavailable. +

+ {/if}
-
- {#if error} -
- {error} -
- {/if} - {#if message} -
- {message} -
- {/if} -
+ {#if hasProfile} +
+ {#if error} +
+ {error} +
+ {/if} + {#if message} +
+ {message} +
+ {/if} +
+ {/if}
{/snippet} @@ -446,7 +463,7 @@ items={[ { value: 'overview', label: 'Overview' }, { value: 'node', label: 'Node', disabled: !lastNodeData }, - { value: 'top-nodes', label: 'Top nodes' } + { value: 'top-nodes', label: 'Top nodes', disabled: !hasProfile } ]} class="px-2" /> @@ -491,11 +508,17 @@ {#if !sqlPanelFullHeight} - - {@render graphPanel()} - - - + {#if hasProfile} + + {@render graphPanel()} + + + {:else} +
+ {@render graphPanel()} +
+ {/if} +
@@ -516,11 +539,21 @@ - - {@render graphPanel()} - - - + {#if hasProfile} + + {@render graphPanel()} + + + {:else} +
+ {@render graphPanel()} +
+ {/if} + {@render analysisPanel()}
@@ -532,15 +565,17 @@ in graphPanel. Surviving the layout toggle (and any other conditional re-render of graphPanel) is what keeps the Visualizer's internal state intact instead of being torn down and re-created from `profileData` each time. --> - - - +{#if profileData} + + + +{/if}