Skip to content

Commit 16af242

Browse files
authored
feat(vite): visualize chunk/assets size in nanovis (#157)
1 parent 78243a9 commit 16af242

File tree

5 files changed

+265
-4
lines changed

5 files changed

+265
-4
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts">
2+
import type { GraphBase, GraphBaseOptions } from 'nanovis'
3+
import type { ChunkChartInfo } from '~/types/chart'
4+
import { useTemplateRef, watchEffect } from 'vue'
5+
6+
const props = defineProps<{
7+
graph: GraphBase<ChunkChartInfo | undefined, GraphBaseOptions<ChunkChartInfo | undefined>>
8+
}>()
9+
10+
const el = useTemplateRef<HTMLDivElement>('el')
11+
12+
watchEffect(() => el.value?.append(props.graph.el))
13+
</script>
14+
15+
<template>
16+
<div ref="el" />
17+
</template>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<script setup lang="ts">
2+
import type { GraphBase, GraphBaseOptions } from 'nanovis'
3+
import type { ChunkChartInfo, ChunkChartNode } from '~/types/chart'
4+
import { colorToCssBackground } from 'nanovis'
5+
import { useTemplateRef, watchEffect } from 'vue'
6+
7+
const props = defineProps<{
8+
graph: GraphBase<ChunkChartInfo | undefined, GraphBaseOptions<ChunkChartInfo | undefined>>
9+
selected?: ChunkChartNode | undefined
10+
}>()
11+
12+
const emit = defineEmits<{
13+
(e: 'select', node: ChunkChartNode | null): void
14+
}>()
15+
16+
const el = useTemplateRef<HTMLDivElement>('el')
17+
watchEffect(() => el.value?.append(props.graph.el))
18+
</script>
19+
20+
<template>
21+
<div grid="~ cols-[max-content_1fr] gap-2" p4>
22+
<div ref="el" w-500px />
23+
<div flex="~ col gap-4">
24+
<ChartNavBreadcrumb
25+
border="b base" py2
26+
:selected="selected"
27+
:options="graph.options"
28+
@select="emit('select', $event)"
29+
/>
30+
<div v-if="selected" grid="~ cols-[250px_1fr] gap-1">
31+
<template v-for="child of selected.children" :key="child.id">
32+
<button
33+
ws-nowrap text-nowrap text-left overflow-hidden text-ellipsis text-sm
34+
hover="bg-active" rounded px2
35+
@click="emit('select', child)"
36+
>
37+
<span v-if="child.meta && child.meta === selected?.meta" text-primary>(self)</span>
38+
<span v-else>{{ child.meta?.name || child.id }}</span>
39+
</button>
40+
41+
<button
42+
relative flex="~ gap-1 items-center"
43+
hover="bg-active" rounded
44+
@click="emit('select', child)"
45+
>
46+
<div
47+
h-5 rounded shadow border="~ base"
48+
:style="{
49+
background: colorToCssBackground(graph.options.getColor?.(child) || '#000'),
50+
width: `${child.size / selected.size * 100}%`,
51+
}"
52+
/>
53+
<DisplayFileSizeBadge text-xs :bytes="child.size" :total="selected.size" :percent-ratio="3" />
54+
<div
55+
v-if="child.children.length > 0"
56+
v-tooltip="`${child.children.length} modules`"
57+
:title="`${child.children.length} modules`"
58+
text-xs op-fade
59+
>
60+
({{ child.children.length }})
61+
</div>
62+
</button>
63+
</template>
64+
</div>
65+
</div>
66+
</div>
67+
</template>

packages/vite/src/app/pages/session/[session]/chunks.vue

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
<script setup lang="ts">
22
import type { RolldownChunkInfo, SessionContext } from '~~/shared/types/data'
33
import type { ClientSettings } from '~/state/settings'
4+
import type { ChunkChartInfo, ChunkChartNode } from '~/types/chart'
45
import { useRpc } from '#imports'
5-
import { computedWithControl, useAsyncState } from '@vueuse/core'
6+
import { computedWithControl, useAsyncState, useMouse } from '@vueuse/core'
67
import Fuse from 'fuse.js'
7-
import { computed, ref } from 'vue'
8+
import { Flamegraph, Sunburst, Treemap } from 'nanovis'
9+
import { computed, reactive, ref, watch } from 'vue'
10+
import ChartTreemap from '~/components/chart/Treemap.vue'
11+
import { useChartGraph } from '~/composables/chart'
812
import { useGraphPathManager } from '~/composables/graph-path-selector'
913
import { settings } from '~/state/settings'
1014
1115
const props = defineProps<{
1216
session: SessionContext
1317
}>()
1418
19+
const mouse = reactive(useMouse())
20+
1521
const chunkViewTypes = [
1622
{
1723
label: 'List',
@@ -28,6 +34,21 @@ const chunkViewTypes = [
2834
value: 'graph',
2935
icon: 'i-ph-graph-duotone',
3036
},
37+
{
38+
label: 'Treemap',
39+
value: 'treemap',
40+
icon: 'i-ph-checkerboard-duotone',
41+
},
42+
{
43+
label: 'Sunburst',
44+
value: 'sunburst',
45+
icon: 'i-ph-chart-donut-duotone',
46+
},
47+
{
48+
label: 'Flamegraph',
49+
value: 'flamegraph',
50+
icon: 'i-ph-chart-bar-horizontal-duotone',
51+
},
3152
] as const
3253
3354
const searchValue = ref<{ search: string | false }>({
@@ -89,6 +110,95 @@ const { pathSelectorVisible, pathNodes, selectPathNodes, togglePathSelector, nor
89110
function toggleDisplay(type: ClientSettings['chunkViewType']) {
90111
settings.value.chunkViewType = type
91112
}
113+
114+
// Calculate chunk size from modules
115+
const modulesMap = computed(() => {
116+
const map = new Map()
117+
for (const module of props.session.modulesList) {
118+
map.set(module.id, module)
119+
}
120+
return map
121+
})
122+
123+
function getChunkSize(chunk: RolldownChunkInfo): number {
124+
// First try to use asset size if available
125+
if (chunk.asset?.size) {
126+
return chunk.asset.size
127+
}
128+
129+
// Otherwise, calculate from module transforms
130+
return chunk.modules.reduce((total, id) => {
131+
const moduleInfo = modulesMap.value.get(id)
132+
if (!moduleInfo || !moduleInfo.buildMetrics?.transforms?.length)
133+
return total
134+
135+
const transforms = moduleInfo.buildMetrics.transforms
136+
return total + transforms[transforms.length - 1]!.transformed_code_size
137+
}, 0)
138+
}
139+
140+
// Normalize chunks with size for chart visualization
141+
const chunksWithSize = computed(() => {
142+
return searched.value.map(chunk => ({
143+
...chunk,
144+
filename: chunk.name || `chunk-${chunk.chunk_id}`,
145+
size: getChunkSize(chunk),
146+
}))
147+
})
148+
149+
// Chart graph setup for nanovis visualizations
150+
const { tree, chartOptions, graph, nodeHover, nodeSelected, selectedNode, selectNode, buildGraph } = useChartGraph<
151+
RolldownChunkInfo & { id: string, filename: string, size: number },
152+
ChunkChartInfo,
153+
ChunkChartNode
154+
>({
155+
data: chunksWithSize,
156+
nameKey: 'filename',
157+
sizeKey: 'size',
158+
rootText: 'Chunks',
159+
nodeType: 'chunk',
160+
graphOptions: {
161+
onClick(node) {
162+
if (node)
163+
nodeHover.value = node
164+
},
165+
onHover(node) {
166+
if (node)
167+
nodeHover.value = node
168+
if (node === null)
169+
nodeHover.value = undefined
170+
},
171+
onLeave() {
172+
nodeHover.value = undefined
173+
},
174+
onSelect(node) {
175+
nodeSelected.value = node || tree.value.root
176+
selectedNode.value = node?.meta
177+
},
178+
},
179+
onUpdate() {
180+
switch (settings.value.chunkViewType) {
181+
case 'sunburst':
182+
graph.value = new Sunburst(tree.value.root, chartOptions.value)
183+
break
184+
case 'treemap':
185+
graph.value = new Treemap(tree.value.root, {
186+
...chartOptions.value,
187+
selectedPaddingRatio: 0,
188+
})
189+
break
190+
case 'flamegraph':
191+
graph.value = new Flamegraph(tree.value.root, chartOptions.value)
192+
break
193+
}
194+
},
195+
})
196+
197+
watch(() => settings.value.chunkViewType, () => {
198+
if (['treemap', 'sunburst', 'flamegraph'].includes(settings.value.chunkViewType)) {
199+
buildGraph()
200+
}
201+
})
92202
</script>
93203

94204
<template>
@@ -166,5 +276,64 @@ function toggleDisplay(type: ClientSettings['chunkViewType']) {
166276
:entry-id="pathNodes.start"
167277
/>
168278
</template>
279+
<template v-else-if="settings.chunkViewType === 'treemap'">
280+
<div of-auto h-screen flex="~ col gap-2" pt32>
281+
<ChartTreemap
282+
v-if="graph" :graph="graph"
283+
:selected="nodeSelected"
284+
@select="x => selectNode(x)"
285+
>
286+
<template #default="{ selected, options, onSelect }">
287+
<ChartNavBreadcrumb
288+
border="b base" py2 min-h-10
289+
:selected="selected"
290+
:options="options"
291+
@select="onSelect"
292+
/>
293+
</template>
294+
</ChartTreemap>
295+
</div>
296+
</template>
297+
<template v-else-if="settings.chunkViewType === 'sunburst'">
298+
<div of-auto h-screen flex="~ col gap-2" pt32>
299+
<ChunksSunburst
300+
v-if="graph" :graph="graph"
301+
:selected="nodeSelected"
302+
@select="x => selectNode(x)"
303+
/>
304+
</div>
305+
</template>
306+
<template v-else-if="settings.chunkViewType === 'flamegraph'">
307+
<div of-auto h-screen flex="~ col gap-2" pt32>
308+
<ChunksFlamegraph
309+
v-if="graph" :graph="graph"
310+
/>
311+
</div>
312+
</template>
313+
<DisplayGraphHoverView :hover-x="mouse.x" :hover-y="mouse.y">
314+
<div
315+
v-if="nodeHover?.meta"
316+
border="~ base rounded-lg" bg-base p2
317+
flex="~ col gap-2"
318+
min-w-50
319+
shadow-lg
320+
>
321+
<div flex="~ gap-2 items-center">
322+
<i i-ph-shapes-duotone flex-none />
323+
<span truncate>{{ nodeHover.meta.name || '[unnamed]' }}</span>
324+
</div>
325+
<div flex="~ gap-2 items-center">
326+
<span op50 text-xs>Size:</span>
327+
<DisplayFileSizeBadge :bytes="nodeHover.meta.size" text-xs />
328+
</div>
329+
<div v-if="nodeHover.meta.modules?.length" flex="~ gap-2 items-center">
330+
<span op50 text-xs>Modules:</span>
331+
<span text-xs>{{ nodeHover.meta.modules?.length }}</span>
332+
</div>
333+
<div v-if="nodeHover.meta.is_initial" flex="~ gap-2 items-center">
334+
<DisplayBadge text="initial" />
335+
</div>
336+
</div>
337+
</DisplayGraphHoverView>
169338
</div>
170339
</template>

packages/vite/src/app/state/settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface ClientSettings {
2222
pluginDetailsModuleTypes: string[] | null
2323
pluginDetailsDurationSortType: string
2424
pluginDetailSelectedHook: string
25-
chunkViewType: 'list' | 'detailed-list' | 'graph'
25+
chunkViewType: 'list' | 'detailed-list' | 'graph' | 'treemap' | 'sunburst' | 'flamegraph'
2626
pluginDetailsShowType: 'changed' | 'unchanged' | 'all'
2727
packageViewType: 'table' | 'treemap' | 'duplicate-packages'
2828
packageSizeSortType: string

packages/vite/src/app/types/chart.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { TreeNode } from 'nanovis'
2-
import type { PackageInfo, PluginBuildInfo, RolldownAssetInfo } from '~~/shared/types'
2+
import type { PackageInfo, PluginBuildInfo, RolldownAssetInfo, RolldownChunkInfo } from '~~/shared/types'
33

44
export type AssetChartInfo = Omit<RolldownAssetInfo, 'type'> & {
55
path: string
@@ -8,6 +8,14 @@ export type AssetChartInfo = Omit<RolldownAssetInfo, 'type'> & {
88

99
export type AssetChartNode = TreeNode<AssetChartInfo | undefined>
1010

11+
export type ChunkChartInfo = Omit<RolldownChunkInfo, 'type'> & {
12+
path: string
13+
type: 'folder' | 'chunk'
14+
size: number
15+
}
16+
17+
export type ChunkChartNode = TreeNode<ChunkChartInfo | undefined>
18+
1119
export type PackageChartInfo = PackageInfo & {
1220
path: string
1321
type: 'folder' | 'package'

0 commit comments

Comments
 (0)