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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@iconify-json/carbon": "catalog:icons",
"@iconify-json/catppuccin": "catalog:icons",
"@iconify-json/codicon": "catalog:icons",
"@iconify-json/fluent": "catalog:icons",
"@iconify-json/logos": "catalog:icons",
"@iconify-json/ph": "catalog:icons",
"@iconify-json/ri": "catalog:icons",
Expand Down
163 changes: 163 additions & 0 deletions packages/devtools-vite/src/app/components/data/PluginDetailsLoader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<script setup lang="ts">
import type { SessionContext } from '~~/shared/types'
import { useRoute } from '#app/composables/router'
import { useRpc } from '#imports'
import { useAsyncState } from '@vueuse/core'
import { computed } from 'vue'
import { settings } from '~~/app/state/settings'

const props = defineProps<{
session: SessionContext
}>()
const emit = defineEmits<{
(e: 'close'): void
}>()

const route = useRoute()
const rpc = useRpc()
const { state, isLoading } = useAsyncState(
async () => {
const res = await rpc.value!['vite:rolldown:get-plugin-details']?.({
session: props.session.id,
id: route.query.plugin as string,
})
return res
},
null,
)

const processedModules = computed(() => {
const seen = new Set()
return state.value?.calls?.filter((call) => {
if (seen.has(call.module)) {
return false
}
seen.add(call.module)
return true
}) ?? []
})

const hookLoadDuration = computed(() => {
const loadMetrics = state.value?.loadMetrics
if (!loadMetrics?.length) {
return
}
return loadMetrics[loadMetrics.length - 1]!.timestamp_end - loadMetrics[0]!.timestamp_start
})

const hookTransformDuration = computed(() => {
const transformMetrics = state.value?.transformMetrics
if (!transformMetrics?.length) {
return
}
return transformMetrics[transformMetrics.length - 1]!.timestamp_end - transformMetrics[0]!.timestamp_start
})

const hookResolveIdDuration = computed(() => {
const resolveIdMetrics = state.value?.resolveIdMetrics
if (!resolveIdMetrics?.length) {
return
}
return resolveIdMetrics[resolveIdMetrics.length - 1]!.timestamp_end - resolveIdMetrics[0]!.timestamp_start
})

const totalDuration = computed(() => {
const calls = state.value?.calls
if (!calls?.length) {
return
}
return calls[calls.length - 1]!.timestamp_end - calls[0]!.timestamp_start
})
</script>

<template>
<VisualLoading v-if="isLoading" />
<div v-else-if="state?.calls?.length" relative h-full w-full>
<DisplayCloseButton
absolute right-2 top-1.5
@click="emit('close')"
/>
<div
bg-glass absolute left-2 top-2 z-panel-content p2
border="~ base rounded-lg"
flex="~ col gap-2"
>
<DisplayPluginName :name="state?.plugin_name!" />
<div text-xs font-mono flex="~ items-center gap-3" ml2>
<DisplayDuration
:duration="hookResolveIdDuration" flex="~ gap-1 items-center"
:title="`Resolve Id hooks cost: ${hookResolveIdDuration}ms`"
>
<span i-ph-magnifying-glass-duotone inline-block />
</DisplayDuration>
<DisplayDuration
:duration="hookLoadDuration" flex="~ gap-1 items-center"
:title="`Load hooks cost: ${hookLoadDuration}ms`"
>
<span i-ph-upload-simple-duotone inline-block />
</DisplayDuration>
<DisplayDuration
:duration="hookTransformDuration" flex="~ gap-1 items-center"
:title="`Transform hooks cost: ${hookTransformDuration}ms`"
>
<span i-ph-magic-wand-duotone inline-block />
</DisplayDuration>
<span op40>|</span>
<DisplayDuration
:duration="totalDuration" flex="~ gap-1 items-center"
:title="`Total build cost: ${totalDuration}ms`"
>
<span i-ph-clock-duotone inline-block />
</DisplayDuration>
<span op40>|</span>
<DisplayNumberBadge
:number="processedModules.length" icon="i-catppuccin-java-class-abstract"
color="transparent color-scale-neutral"
:title="`Module processed: ${processedModules.length}`"
/>
<span op40>|</span>
<DisplayNumberBadge
:number="state?.calls?.length ?? 0" icon="i-ph:arrow-counter-clockwise"
color="transparent color-scale-neutral"
:title="`Total calls: ${state?.calls?.length ?? 0}`"
/>
</div>
<div flex="~ gap-2">
<button
:class="settings.pluginDetailsViewType === 'flow' ? 'text-primary' : ''"
flex="~ gap-2 items-center justify-center"
px2 py1 w-40
border="~ base rounded-lg"
hover="bg-active"
@click="settings.pluginDetailsViewType = 'flow'"
>
<div i-ph-git-branch-duotone rotate-180 />
Build Flow
</button>
<button
:class="settings.pluginDetailsViewType === 'charts' ? 'text-primary' : ''"
flex="~ gap-2 items-center justify-center"
px2 py1 w-40
border="~ base rounded-lg"
hover="bg-active"
@click="settings.pluginDetailsViewType = 'charts'"
>
<div i-ph-chart-donut-duotone />
Charts
</button>
</div>
</div>
<div of-auto h-full pt-30>
<FlowmapPluginFlow
v-if="settings.pluginDetailsViewType === 'flow'"
:session="session"
:build-metrics="state"
/>
</div>
</div>
<div v-else flex="~ items-center justify-center" w-full h-full>
<span italic op50>
No data
</span>
</div>
</template>
174 changes: 174 additions & 0 deletions packages/devtools-vite/src/app/components/data/PluginDetailsTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<script setup lang="ts">
import type { RolldownPluginBuildMetrics, SessionContext } from '~~/shared/types/data'
import type { FilterMatchRule } from '~/utils/icon'
import { useCycleList } from '@vueuse/core'
import { Menu as VMenu } from 'floating-vue'
import { computed, ref } from 'vue'
import { settings } from '~~/app/state/settings'
import { parseReadablePath } from '~/utils/filepath'
import { getFileTypeFromModuleId, ModuleTypeRules } from '~/utils/icon'

const props = defineProps<{
session: SessionContext
buildMetrics: RolldownPluginBuildMetrics
selectedFields: string[]
}>()

const HOOK_NAME_MAP = {
resolve: 'Resolve Id',
load: 'Load',
transform: 'Transform',
}

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 searchFilterTypes = computed(() => ModuleTypeRules.filter(rule => parsedPaths.value.some(mod => rule.match.test(mod.mod.id))))

const filterModuleTypes = ref<string[]>(settings.value.pluginDetailsModuleTypes ?? searchFilterTypes.value.map(i => i.name))
const { state: durationSortType, next } = useCycleList(['', 'desc', 'asc'], {
initialValue: settings.value.pluginDetailsDurationSortType,
})
const filtered = computed(() => {
const sorted = durationSortType.value
? [...props.buildMetrics.calls].sort((a, b) => {
if (durationSortType.value === 'asc') {
return a.duration - b.duration
}
return b.duration - a.duration
})
: props.buildMetrics.calls
return sorted.filter((i) => {
const matched = getFileTypeFromModuleId(i.module)
return filterModuleTypes.value.includes(matched.name)
}).filter(settings.value.pluginDetailSelectedHook ? i => i.type === settings.value.pluginDetailSelectedHook : Boolean)
})

function toggleModuleType(rule: FilterMatchRule) {
if (filterModuleTypes.value?.includes(rule.name)) {
filterModuleTypes.value = filterModuleTypes.value?.filter(t => t !== rule.name)
}
else {
filterModuleTypes.value?.push(rule.name)
}
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
}
</script>

<template>
<table w-full border-separate border-spacing-0>
<thead border="b base">
<tr px2 class="[&_th]:(sticky top-0 z10 border-b border-base)">
<th v-if="selectedFields.includes('hookName')" bg-base w32 ws-nowrap p1 text-center font-600>
Hook name
</th>
<th v-if="selectedFields.includes('module')" bg-base min-w100 ws-nowrap p1 text-left font-600>
<button flex="~ row gap1 items-center" w-full>
Module
<VMenu>
<span w-6 h-6 rounded-full cursor-pointer hover="bg-active" flex="~ items-center justify-center">
<i text-xs class="i-carbon-filter" :class="filterModuleTypes.length !== searchFilterTypes.length ? 'text-primary op100' : 'op50'" />
</span>
<template #popper>
<div class="p2" flex="~ col gap2">
<label
v-for="rule of searchFilterTypes"
:key="rule.name"
border="~ base rounded-md" px2 py1
flex="~ items-center gap-1"
select-none
:title="rule.description"
class="cursor-pointer module-type-filter"
>
<input
type="checkbox"
mr1
:checked="filterModuleTypes?.includes(rule.name)"
@change="toggleModuleType(rule)"
>
<div :class="rule.icon" icon-catppuccin />
<div text-sm>{{ rule.description || rule.name }}</div>
</label>
</div>
</template>
</VMenu>
</button>
</th>
<th v-if="selectedFields.includes('startTime')" rounded-tr-2 bg-base ws-nowrap p1 text-center font-600>
Start Time
</th>
<th v-if="selectedFields.includes('endTime')" rounded-tr-2 bg-base ws-nowrap p1 text-center font-600>
End Time
</th>
<th v-if="selectedFields.includes('duration')" rounded-tr-2 bg-base ws-nowrap p1 text-center font-600>
<button flex="~ row gap1 items-center justify-center" w-full @click="toggleDurationSortType">
Duration
<span w-6 h-6 rounded-full cursor-pointer hover="bg-active" flex="~ items-center justify-center">
<i text-xs :class="[durationSortType !== 'asc' ? 'i-carbon-arrow-down' : 'i-carbon-arrow-up', durationSortType ? 'op100 text-primary' : 'op50']" />
</span>
</button>
</th>
</tr>
</thead>
<tbody v-if="filtered.length">
<tr v-for="(item, index) in filtered" :key="item.id" class="[&_td]:(border-base border-b-1 border-dashed)" :class="[index === filtered.length - 1 ? '[&_td]:(border-b-0)' : '']">
<td v-if="selectedFields.includes('hookName')" w32 ws-nowrap text-center text-sm op80>
{{ HOOK_NAME_MAP[item.type] }}
</td>
<td v-if="selectedFields.includes('module')" min-w100 text-left text-ellipsis line-clamp-2>
<DisplayModuleId
:id="item.module"
w-full border-none
:session="session"
:link="`/session/${session.id}/graph?module=${item.module}`"
hover="bg-active"
border="~ base rounded" block px2 py1
/>
</td>
<td v-if="selectedFields.includes('startTime')" text-center font-mono text-sm min-w52 op80>
<time v-if="item.timestamp_start" :datetime="new Date(item.timestamp_start).toISOString()">{{ normalizeTimestamp(item.timestamp_start) }}</time>
</td>
<td v-if="selectedFields.includes('endTime')" text-center font-mono text-sm min-w52 op80>
<time v-if="item.timestamp_end" :datetime="new Date(item.timestamp_end).toISOString()">{{ normalizeTimestamp(item.timestamp_end) }}</time>
</td>
<td v-if="selectedFields.includes('duration')" text-center text-sm>
<DisplayDuration :duration="item.duration" />
</td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td :colspan="selectedFields.length" p4>
<div w-full h-48 flex="~ items-center justify-center" op50 italic>
No data
</div>
</td>
</tr>
</tbody>
</table>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import type { FilterMatchRule } from '~/utils/icon'
import { useVModel } from '@vueuse/core'
import { withDefaults } from 'vue'

interface ModelValue { search: string, selected?: string[] | null }
interface ModelValue { search?: string | false, selected?: string[] | null }

const props = withDefaults(
defineProps<{
rules: FilterMatchRule[]
modelValue?: ModelValue
selectedContainerClass?: string
}>(),
{
modelValue: () => ({
search: '',
selected: null,
}),
selectedContainerClass: '',
},
)

Expand Down Expand Up @@ -71,7 +73,7 @@ function unselectToggle() {

<template>
<div flex="col gap-2" max-w-90vw min-w-30vw border="~ base rounded-xl" bg-glass>
<div>
<div v-if="modelValue.search !== false">
<input
v-model="model.search"
p2 px4
Expand All @@ -80,7 +82,7 @@ function unselectToggle() {
placeholder="Search"
>
</div>
<div v-if="rules.length" flex="~ gap-2 wrap" p2 border="t base">
<div v-if="rules.length" :class="selectedContainerClass" flex="~ gap-2 wrap" p2 border="t base">
<label
v-for="rule of rules"
:key="rule.name"
Expand Down
Loading
Loading