Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ const tree = computed(() => {
const children = [
n({
id: '~resolves',
text: 'resolve',
text: 'Resolve Id',
children: resolveIds,
}),
n({
id: '~loads',
text: 'load',
text: 'Load',
children: loads,
}),
n({
id: '~transforms',
text: 'transform',
text: 'Transform',
children: transforms,
}),
]
Expand Down
196 changes: 196 additions & 0 deletions packages/devtools-vite/src/app/components/chart/PluginFlamegraph.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<script setup lang="ts">
import type { TreeNodeInput } from 'nanovis'
import type { PluginBuildInfo, RolldownPluginBuildMetrics, SessionContext } from '~~/shared/types'
import { Flamegraph, normalizeTreeNode } from 'nanovis'
import { relative } from 'pathe'
import { computed, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
import { parseReadablePath } from '~/utils/filepath'
import { normalizeTimestamp } from '~/utils/format'
import { getFileTypeFromModuleId, ModuleTypeRules } from '~/utils/icon'

const props = defineProps<{
session: SessionContext
buildMetrics: RolldownPluginBuildMetrics
}>()

const parsedPaths = computed(() => props.session.modulesList.map((mod) => {
const path = parseReadablePath(mod.id, props.session.meta.cwd)
const type = getFileTypeFromModuleId(mod.id)
return {
mod,
path,
type,
}
}))
const moduleTypes = computed(() => ModuleTypeRules.filter(rule => parsedPaths.value.some(mod => rule.match.test(mod.mod.id))))

const n = (node: TreeNodeInput<PluginBuildInfo>) => normalizeTreeNode(node, undefined, false)

function normalizeModulePath(path: string) {
const normalized = path.replace(/%2F/g, '/')
const cwd = props.session!.meta.cwd
let relate = cwd ? relative(cwd, normalized) : normalized
if (!relate.startsWith('.'))
relate = `./${relate}`
if (relate.startsWith('./'))
return relate
if (relate.match(/^(?:\.\.\/){1,3}[^.]/))
return relate
return normalized
}

const tree = computed(() => {
const resolveIds = moduleTypes.value.map((type, idx) => n({
id: `resolveId-${type.name}-${idx}`,
text: type.description,
children: props.buildMetrics.resolveIdMetrics.filter((item) => {
return getFileTypeFromModuleId(item.module).name === type.name
}).map((id, idx) => n({
id: `resolveId-${idx}`,
text: normalizeModulePath(id.module),
size: id.duration,
})),
}))
const loads = moduleTypes.value.map((type, idx) => n({
id: `loads-${type.name}-${idx}`,
text: type.description,
children: props.buildMetrics.loadMetrics.filter((item) => {
return getFileTypeFromModuleId(item.module).name === type.name
}).map((id, idx) => n({
id: `resolveId-${idx}`,
text: normalizeModulePath(id.module),
size: id.duration,
})),
}))
const transforms = moduleTypes.value.map((type, idx) => n({
id: `transforms-${type.name}-${idx}`,
text: type.description,
children: props.buildMetrics.transformMetrics.filter((item) => {
return getFileTypeFromModuleId(item.module).name === type.name
}).map((id, idx) => n({
id: `resolveId-${idx}`,
text: normalizeModulePath(id.module),
size: id.duration,
})),
}))

// resolve/load/transform -> module type -> module
const children = [
n({
id: '~resolves',
text: 'Resolve Id',
children: resolveIds,
}),
n({
id: '~loads',
text: 'Load',
children: loads,
}),
n({
id: '~transforms',
text: 'Transform',
children: transforms,
}),
]

return n({
id: '~root',
text: 'Plugin Flamegraph',
children,
})
})

const hoverNode = ref<{
plugin_name: string
duration: number
meta: PluginBuildInfo | undefined
} | null>(null)
const hoverX = ref<number>(0)
const hoverY = ref<number>(0)
const el = useTemplateRef<HTMLDivElement>('el')
const flamegraph = shallowRef<Flamegraph<PluginBuildInfo> | null>(null)

function buildFlamegraph() {
flamegraph.value = new Flamegraph(tree.value, {
animate: true,
palette: {
fg: '#888',
},
getSubtext: (node) => {
const p = node.size / tree.value.size * 100
if (p > 15 && p !== 100) {
return `${p.toFixed(1)}%`
}
return undefined
},
onHover(node, e) {
if (!node) {
hoverNode.value = null
return
}
if (e) {
hoverX.value = e.clientX
hoverY.value = e.clientY
}
hoverNode.value = {
plugin_name: node.text!,
duration: node.size,
meta: node.meta,
}
},
})
el.value!.appendChild(flamegraph.value!.el)
}

function disposeFlamegraph() {
flamegraph.value?.dispose()
}

onMounted(() => {
buildFlamegraph()
})

onUnmounted(() => {
disposeFlamegraph()
})

watch(tree, async () => {
disposeFlamegraph()
buildFlamegraph()
}, {
deep: true,
})
</script>

<template>
<div relative border="t base" pb10 py1 mt4>
<Teleport to="body">
<div
v-if="hoverNode"
border="~ base" rounded shadow px2 py1 fixed
z-panel-content bg-glass pointer-events-none text-sm
:style="{ left: `${hoverX}px`, top: `${hoverY}px` }"
>
<div flex="~" font-bold font-mono>
<DisplayFileIcon v-if="hoverNode.meta" :filename="hoverNode.meta.module" mr1.5 />
{{ hoverNode.plugin_name }}
</div>
<div v-if="hoverNode.meta">
<div>
<label>Start Time: </label>
<time :datetime="new Date(hoverNode.meta.timestamp_start).toISOString()">{{ normalizeTimestamp(hoverNode.meta.timestamp_start) }}</time>
</div>
<div>
<label>End Time: </label>
<time :datetime="new Date(hoverNode.meta.timestamp_end).toISOString()">{{ normalizeTimestamp(hoverNode.meta.timestamp_end) }}</time>
</div>
</div>
<div flex="~ gap-1">
<label>Duration: </label>
<DisplayDuration :duration="hoverNode.duration" />
</div>
</div>
</Teleport>
<div ref="el" min-h-30 />
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ const totalDuration = computed(() => {
:session="session"
:build-metrics="state"
/>
<ChartPluginFlamegraph
v-if="settings.pluginDetailsViewType === 'charts'"
:session="session"
:build-metrics="state"
/>
</div>
</div>
<div v-else flex="~ items-center justify-center" w-full h-full>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Menu as VMenu } from 'floating-vue'
import { computed, ref } from 'vue'
import { settings } from '~~/app/state/settings'
import { parseReadablePath } from '~/utils/filepath'
import { normalizeTimestamp } from '~/utils/format'
import { getFileTypeFromModuleId, ModuleTypeRules } from '~/utils/icon'

const props = defineProps<{
Expand Down Expand Up @@ -63,19 +64,6 @@ function toggleModuleType(rule: FilterMatchRule) {
settings.value.pluginDetailsModuleTypes = filterModuleTypes.value
}

function normalizeTimestamp(timestamp: number) {
return new Date(timestamp).toLocaleString(undefined, {
hour12: false,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
})
}

function toggleDurationSortType() {
next()
settings.value.pluginDetailsDurationSortType = durationSortType.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,3 @@ debouncedWatch(
</div>
</div>
</template>

<!--
TODO: plugins framegraph
Two different views direction:
- plugins -> hooks -> modules
- modules -> hooks -> plugins
-->
13 changes: 13 additions & 0 deletions packages/devtools-vite/src/app/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,16 @@ export function toTree(modules: ModuleDest[], name: string) {

return node
}

export function normalizeTimestamp(timestamp: number) {
return new Date(timestamp).toLocaleString(undefined, {
hour12: false,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
})
}
Loading