Skip to content
Open
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
@@ -0,0 +1,67 @@
import type { Event } from '@rolldown/debug'
import { describe, expect, it } from 'vitest'
import { createPluginBuildMetricsSummary, RolldownEventsManager } from '../events-manager'
import { stringifyInSlices } from '../log-cache'

const base = { build_id: 'bid_1', session_id: 'sid_1' }

function hookCalls(callId: string, pluginId: number, module: string, time: number): Record<string, unknown>[] {
const plugin = { plugin_name: `plugin-${pluginId}`, plugin_id: pluginId, trigger: 'automatic', ...base }
return [
{ timestamp: time, action: 'HookResolveIdCallStart', importer: null, module_request: module, import_kind: 'import-statement', call_id: `${callId}-r`, ...plugin },
{ timestamp: time + 1, action: 'HookResolveIdCallEnd', resolved_id: module, is_external: false, call_id: `${callId}-r`, ...plugin },
{ timestamp: time + 2, action: 'HookLoadCallStart', module_id: module, call_id: `${callId}-l`, ...plugin },
{ timestamp: time + 5, action: 'HookLoadCallEnd', module_id: module, content: null, call_id: `${callId}-l`, ...plugin },
{ timestamp: time + 6, action: 'HookTransformCallStart', module_id: module, content: 'a', call_id: `${callId}-t`, ...plugin },
{ timestamp: time + 9, action: 'HookTransformCallEnd', module_id: module, content: 'bb', call_id: `${callId}-t`, ...plugin },
]
}

function expectSummariesMatchCalls(manager: RolldownEventsManager) {
expect(manager.plugin_build_metrics.size).toBeGreaterThan(0)
const expected = new Map(Array.from(manager.plugin_build_metrics, ([id, metrics]) => [id, createPluginBuildMetricsSummary(metrics)]))
expect(manager.plugin_build_summaries).toEqual(expected)
}

describe('plugin summaries accumulated while parsing', () => {
it('match summaries computed from the recorded calls, also after a restore', () => {
const manager = new RolldownEventsManager()
const events = [
{ timestamp: 1, action: 'BuildStart', ...base },
...hookCalls('a', 1, '/src/a.ts', 10),
...hookCalls('b', 2, '/src/b.ts', 20),
...hookCalls('c', 1, '/src/c.ts', 40),
// An end without a matching start isn't recorded, so it must not be counted either.
{ timestamp: 60, action: 'HookTransformCallEnd', module_id: '/src/a.ts', content: 'x', call_id: 'missing', plugin_name: 'plugin-1', plugin_id: 1, trigger: 'automatic', ...base },
]
for (const event of events)
manager.handleEvent(event as unknown as Event)

expectSummariesMatchCalls(manager)
expect(manager.plugin_build_summaries.get(1)!.total.count).toBe(6)

// Restoring a snapshot (package summary cache) rebuilds the summaries.
const restored = new RolldownEventsManager()
restored.restore(manager.snapshot())
expect(restored.plugin_build_summaries).toEqual(manager.plugin_build_summaries)
})
})

describe('sliced cache serialization', () => {
it('produces the same JSON as JSON.stringify', async () => {
const large = Array.from({ length: 1100 }, (_, index) => index % 7 === 0
? undefined
: {
id: `m${index} "quoted" \u2028 ä`,
skipped: undefined,
nested: index % 5 ? [index, null, undefined, { deep: [1, 2] }] : Array.from({ length: 1100 }, (_, i) => i),
fn: () => index,
// eslint-disable-next-line no-new-wrappers, unicorn/new-for-builtins
boxed: new String(`s${index}`),
date: new Date(index),
})
const value = { version: 3, log: { size: 1, mtime: 2 }, data: { metrics: large, empty: [], text: 'x' }, missing: undefined }

expect((await stringifyInSlices(value)).join('')).toBe(JSON.stringify(value))
})
})
71 changes: 71 additions & 0 deletions packages/rolldown/src/node/rolldown/events-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,62 @@ function getContentInfo(content: string | null | undefined): ContentInfo {
}
}

export interface PluginMetricSummary {
count: number
duration: number
}

export interface PluginBuildMetricsSummary {
plugin_id: number
plugin_name: string
total: PluginMetricSummary
resolve: PluginMetricSummary
load: PluginMetricSummary
transform: PluginMetricSummary
}

function createMetricSummary(): PluginMetricSummary {
return {
count: 0,
duration: 0,
}
}

function addMetricSummary(summary: PluginMetricSummary, duration: number) {
summary.count += 1
summary.duration += duration
}

function createEmptyPluginBuildMetricsSummary(pluginId: number, pluginName: string): PluginBuildMetricsSummary {
return {
plugin_id: pluginId,
plugin_name: pluginName,
total: createMetricSummary(),
resolve: createMetricSummary(),
load: createMetricSummary(),
transform: createMetricSummary(),
}
}

function addPluginCallToSummary(summary: PluginBuildMetricsSummary, call: Pick<PluginBuildMetrics['calls'][number], 'type' | 'duration'>) {
addMetricSummary(summary.total, call.duration)
if (call.type === 'resolve')
addMetricSummary(summary.resolve, call.duration)
else if (call.type === 'load')
addMetricSummary(summary.load, call.duration)
else if (call.type === 'transform')
addMetricSummary(summary.transform, call.duration)
}

export function createPluginBuildMetricsSummary(metrics: PluginBuildMetrics): PluginBuildMetricsSummary {
const summary = createEmptyPluginBuildMetricsSummary(metrics.plugin_id, metrics.plugin_name)

for (const call of metrics.calls)
addPluginCallToSummary(summary, call)

return summary
}

export function getContentRef(content: unknown) {
return typeof content === 'string' && content.startsWith('$ref:')
? content.slice(5)
Expand Down Expand Up @@ -106,6 +162,8 @@ export class RolldownEventsManager {
module_build_hook_events: Map<string, PendingModuleBuildHookEvent> = new Map()
module_build_metrics: Map<string, ModuleBuildMetrics> = new Map()
plugin_build_metrics: Map<number, PluginBuildMetrics> = new Map()
// Kept in step with `plugin_build_metrics`, so writing the plugin summary cache doesn't rescan every call.
plugin_build_summaries: Map<number, PluginBuildMetricsSummary> = new Map()
build_start_time: number = 0
build_end_time: number = 0

Expand All @@ -124,6 +182,15 @@ export class RolldownEventsManager {
: getContentInfo(undefined)
}

private addPluginCallSummary(pluginId: number, pluginName: string, duration: number, type: PluginBuildMetrics['calls'][number]['type']) {
let summary = this.plugin_build_summaries.get(pluginId)
if (!summary) {
summary = createEmptyPluginBuildMetricsSummary(pluginId, pluginName)
this.plugin_build_summaries.set(pluginId, summary)
Comment on lines +188 to +189
}
addPluginCallToSummary(summary, { type, duration })
}

recordBuildMetrics(event: ModuleBuildHookEvents) {
if (MODULE_BUILD_START_HOOKS.includes(event.action)) {
if (event.action === 'HookResolveIdCallStart') {
Expand Down Expand Up @@ -225,6 +292,8 @@ export class RolldownEventsManager {
})
}
this.plugin_build_metrics.set(pluginId, plugin_build_metrics)
// Every branch above either returned or recorded a call.
this.addPluginCallSummary(pluginId, event.plugin_name, info.duration, plugin_build_metrics.calls.at(-1)!.type)
this.module_build_metrics.set(module_id, module_build_metrics)
}
}
Expand Down Expand Up @@ -320,6 +389,7 @@ export class RolldownEventsManager {
this.module_build_hook_events.clear()
this.module_build_metrics.clear()
this.plugin_build_metrics.clear()
this.plugin_build_summaries.clear()
this.build_start_time = 0
this.build_end_time = 0
}
Expand Down Expand Up @@ -360,6 +430,7 @@ export class RolldownEventsManager {
build_metrics: this.module_build_metrics.get(id),
}]))
this.plugin_build_metrics = new Map(snapshot.plugin_build_metrics)
this.plugin_build_summaries = new Map(snapshot.plugin_build_metrics.map(([pluginId, metrics]) => [pluginId, createPluginBuildMetricsSummary(metrics)]))
this.build_start_time = snapshot.build_start_time
this.build_end_time = snapshot.build_end_time
}
Expand Down
Loading
Loading