diff --git a/packages/vite/src/node/__tests__/inspect-context.test.ts b/packages/vite/src/node/__tests__/inspect-context.test.ts index 9c863e8dd..9f37eb8bd 100644 --- a/packages/vite/src/node/__tests__/inspect-context.test.ts +++ b/packages/vite/src/node/__tests__/inspect-context.test.ts @@ -1030,4 +1030,130 @@ describe('vite inspect context', () => { ], }) }) + it.each(['memory', 'disk'])('counts declined loads without replacing code (%s)', async (storage) => { + const dir = mkdtempSync(join(tmpdir(), 'vite-load-timing-')) + const { ctx, envCtx, vite } = await createFixture({ store: { filename: storage === 'disk' ? join(dir, 'payloads.bin') : ':memory:', maxBatchItems: 1 } }) + try { + const id = '/src/load-timing.ts' + const source = 'export const value = 1' + const record = () => { + envCtx.recordLoadCall(id, { name: 'plugin-a', start: 0, end: 200 }, vite.config.plugins[1]) + envCtx.recordLoad(id, { name: 'vite:load-fallback', result: source, start: 200, end: 202 }, vite.config.plugins[0]) + envCtx.recordTransform(id, { name: 'plugin-b', result: source, start: 202, end: 205 }, source, vite.config.plugins[2]) + } + record() + const assertMetrics = async (calls = 1) => { + const [module] = await envCtx.getModulesList() + expect(module).toMatchObject({ totalTime: 205, virtual: false, sourceSize: source.length, distSize: source.length }) + expect(module!.plugins).toContainEqual({ name: 'plugin-a', transform: 200 }) + expect((await envCtx.getModuleTransformInfo(id)).transforms.map(t => t.name)).toEqual(['vite:load-fallback', 'plugin-b']) + expect((await envCtx.getPluginDetails(1)).loadMetrics).toHaveLength(calls) + } + await assertMetrics() + record() + await assertMetrics(2) + envCtx.invalidate(id) + expect(await envCtx.getModulesList()).toEqual([]) + record() + await assertMetrics() + envCtx.clearScope() + expect(await envCtx.getModulesList()).toEqual([]) + } + finally { + await ctx.close() + rmSync(dir, { recursive: true, force: true }) + } + }) + + it.each(['memory', 'disk'])('isolates overlapping load requests (%s)', async (storage) => { + const dir = mkdtempSync(join(tmpdir(), 'vite-load-requests-')) + const { ctx, envCtx } = await createFixture({ store: { + filename: storage === 'disk' ? join(dir, 'payloads.bin') : ':memory:', + maxBatchItems: storage === 'disk' ? 1 : undefined, + } }) + try { + const id = '/src/load-requests.ts' + const source = 'export const value = 1' + const failedRequest = {} + const successfulRequest = {} + const recordCall = (request: object, duration: number) => { + ctx.store.recordPluginCall(envCtx.scope, { + type: 'load', + id: `load:${duration}`, + duration, + plugin_id: 1, + plugin_name: 'plugin-a', + module: id, + timestamp_start: 0, + timestamp_end: duration, + unchanged: true, + }, request) + } + recordCall(failedRequest, 100) + recordCall(successfulRequest, 20) + ctx.store.clearPendingLoadMetrics(envCtx.scope, failedRequest) + ctx.store.recordLoad(envCtx.scope, id, id, { + name: 'vite:load-fallback', + result: source, + start: 20, + end: 22, + }, undefined, successfulRequest) + ctx.store.clearPendingLoadMetrics(envCtx.scope, successfulRequest) + + const [module] = await envCtx.getModulesList() + expect(module).toMatchObject({ totalTime: 22, sourceSize: source.length, distSize: source.length }) + expect(module!.plugins).toContainEqual({ name: 'plugin-a', transform: 20 }) + expect((await envCtx.getPluginDetails(1)).loadMetrics).toHaveLength(2) + } + finally { + await ctx.close() + rmSync(dir, { recursive: true, force: true }) + } + }) + + it.each(['memory', 'disk'])('replaces a previous request source within a write batch (%s)', async (storage) => { + const dir = mkdtempSync(join(tmpdir(), 'vite-source-retry-')) + const { ctx, envCtx } = await createFixture({ store: { + filename: storage === 'disk' ? join(dir, 'payloads.bin') : ':memory:', + } }) + try { + const id = '/src/source-retry.ts' + const source = 'export const value = 1' + const output = 'export const value = 2' + const failedRequest = {} + const successfulRequest = {} + ctx.store.recordLoad(envCtx.scope, id, id, { + name: 'failed-load', + result: '[Error]', + start: 0, + end: 80, + }, undefined, failedRequest) + ctx.store.recordTransform(envCtx.scope, id, id, { + name: 'plugin-a', + start: 80, + end: 83, + }, source, undefined, successfulRequest) + ctx.store.recordTransform(envCtx.scope, id, id, { + name: 'plugin-b', + result: output, + start: 83, + end: 85, + }, source, undefined, successfulRequest) + ctx.store.clearPendingLoadMetrics(envCtx.scope, failedRequest) + ctx.store.clearPendingLoadMetrics(envCtx.scope, successfulRequest) + + expect((await envCtx.getModuleTransformInfo(id)).transforms).toMatchObject([ + { name: '__load__', result: source }, + { name: 'plugin-a' }, + { name: 'plugin-b', result: output }, + ]) + expect(await envCtx.getModulesList()).toMatchObject([ + { id, sourceSize: source.length, distSize: output.length, invokeCount: 2 }, + ]) + } + finally { + await ctx.close() + rmSync(dir, { recursive: true, force: true }) + } + }) }) diff --git a/packages/vite/src/node/__tests__/inspect-server.test.ts b/packages/vite/src/node/__tests__/inspect-server.test.ts index d4b5dc7c9..3158025b9 100644 --- a/packages/vite/src/node/__tests__/inspect-server.test.ts +++ b/packages/vite/src/node/__tests__/inspect-server.test.ts @@ -1,6 +1,9 @@ import type { Environment, Plugin, ViteDevServer } from 'vite' import type { ViteInspectEnvironmentContext } from '../inspect/context' import type { ViteInspectStoreOptions } from '../inspect/store' +import { mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { createServer } from 'vite' import { afterEach, describe, expect, it, vi } from 'vitest' import { ViteInspectContext } from '../inspect/context' @@ -15,12 +18,15 @@ interface InspectServerFixture { } const fixtures: InspectServerFixture[] = [] +const temporaryDirs: string[] = [] afterEach(async () => { await Promise.all(fixtures.splice(0).map(async ({ server, inspectContext }) => { await server.close() await inspectContext.close() })) + for (const dir of temporaryDirs.splice(0)) + rmSync(dir, { recursive: true, force: true }) }) async function createInspectServer( @@ -332,4 +338,151 @@ describe('vite inspect server invalidation', () => { expect(lateRequest).toBeUndefined() }) + it.each(['memory', 'disk'])('excludes failed loads from a successful retry (%s)', async (storage) => { + const dir = mkdtempSync(join(tmpdir(), 'vite-load-retry-')) + temporaryDirs.push(dir) + const id = '/load-retry.js' + const source = 'export const value = 1' + let enabled = false + const plugin: Plugin = { + name: 'test:retry-load', + enforce: 'pre', + async load(moduleId) { + if (moduleId !== id) + return + await new Promise(resolve => setTimeout(resolve, 20)) + }, + } + const provider: Plugin = { + name: 'test:retry-provider', + resolveId(moduleId) { + if (moduleId === id) + return id + }, + load(moduleId) { + if (moduleId === id && enabled) + return source + }, + } + const { server, envContext } = await createInspectServer(new Map(), [plugin, provider], { + filename: storage === 'disk' ? join(dir, 'payloads.bin') : ':memory:', + maxBatchItems: storage === 'disk' ? 1 : undefined, + }) + await expect(server.transformRequest(id)).rejects.toThrow() + enabled = true + await server.transformRequest(id) + + const details = await envContext.getPluginDetails(server.config.plugins.indexOf(plugin)) + expect(details.loadMetrics).toHaveLength(2) + expect(details.loadMetrics[0]!.duration).toBeGreaterThan(0) + const duration = details.loadMetrics[1]!.duration + expect(duration).toBeGreaterThan(0) + const module = (await envContext.getModulesList()).find(module => module.id === id)! + expect(module.plugins.filter(p => p.name === plugin.name)).toEqual([{ name: plugin.name, transform: duration }]) + expect(module.sourceSize).toBe(source.length) + expect((await envContext.getModuleTransformInfo(id)).transforms[0]?.result).toBe(source) + }) + + it.each(['memory', 'disk'])('replaces failed load timings when a retry reads a file (%s)', async (storage) => { + const dir = realpathSync(mkdtempSync(join(tmpdir(), 'vite-file-load-retry-'))) + temporaryDirs.push(dir) + const id = join(dir, 'entry.js').replace(/\\/g, '/') + const source = 'export const value = 1' + writeFileSync(id, source) + let fail = true + let now = Date.now() + const clock = vi.spyOn(Date, 'now').mockImplementation(() => now) + const declined: Plugin = { + name: 'test:declined-file-load', + enforce: 'pre', + load(moduleId) { + if (moduleId === id) + now += fail ? 80 : 5 + }, + } + const failing: Plugin = { + name: 'test:failing-file-load', + enforce: 'pre', + load(moduleId) { + if (moduleId === id && fail) + this.error('First load attempt failed') + }, + } + try { + const { server, envContext } = await createInspectServer(new Map(), [declined, failing], { + filename: storage === 'disk' ? join(dir, 'payloads.bin') : ':memory:', + maxBatchItems: storage === 'disk' ? 1 : undefined, + }) + await expect(server.transformRequest(id)).rejects.toThrow('First load attempt failed') + // Persist the failed snapshot before the retry starts. + await envContext.getModulesList() + fail = false + await server.transformRequest(id) + + const details = await envContext.getPluginDetails(server.config.plugins.indexOf(declined)) + expect(details.loadMetrics.map(call => call.duration)).toEqual([80, 5]) + const module = (await envContext.getModulesList()).find(module => module.id === id)! + expect(module.plugins.filter(plugin => plugin.name === declined.name)).toEqual([ + { name: declined.name, transform: 5 }, + ]) + expect(module).toMatchObject({ sourceSize: source.length, distSize: source.length, virtual: false }) + const { transforms } = await envContext.getModuleTransformInfo(id) + expect(transforms[0]).toMatchObject({ name: '__load__', result: source }) + expect(transforms.some(transform => transform.result === '[Error]')).toBe(false) + } + finally { + clock.mockRestore() + } + }) + + it.each([undefined, null, 'code'] as const)('counts load time with result %s through Vite', async (result) => { + const id = '/load-timing.js' + const source = 'export const value = 1' + const plugin: Plugin = { + name: 'test:slow-load', + enforce: 'pre', + async load(moduleId) { + if (moduleId !== id) + return + await new Promise(resolve => setTimeout(resolve, 20)) + return result === 'code' ? source : result + }, + } + const { server, envContext } = await createInspectServer(new Map([[id, source]]), [plugin]) + await server.transformRequest(id) + const details = await envContext.getPluginDetails(server.config.plugins.indexOf(plugin)) + const duration = details.loadMetrics.reduce((sum, call) => sum + call.duration, 0) + expect(duration).toBeGreaterThan(0) + const [module] = await envContext.getModulesList() + expect(module!.plugins.filter(p => p.name === plugin.name).reduce((sum, p) => sum + (p.transform ?? 0), 0)).toBe(duration) + expect(module!.sourceSize).toBe(source.length) + expect(module!.distSize).toBe(source.length) + }) + + it.each([undefined, null])('counts declined loads before Vite reads a file (%s)', async (result) => { + const dir = realpathSync(mkdtempSync(join(tmpdir(), 'vite-file-load-'))) + temporaryDirs.push(dir) + const id = join(dir, 'entry.js').replace(/\\/g, '/') + const source = 'export const value = 1' + writeFileSync(id, source) + const plugin: Plugin = { + name: 'test:declined-file-load', + enforce: 'pre', + async load(moduleId) { + if (moduleId !== id) + return + await new Promise(resolve => setTimeout(resolve, 20)) + return result + }, + } + const { server, envContext } = await createInspectServer(new Map(), [plugin]) + await server.transformRequest(id) + const details = await envContext.getPluginDetails(server.config.plugins.indexOf(plugin)) + const duration = details.loadMetrics.reduce((sum, call) => sum + call.duration, 0) + expect(duration).toBeGreaterThan(0) + const [module] = await envContext.getModulesList() + expect(module!.plugins).toContainEqual({ name: plugin.name, transform: duration }) + expect(module!.sourceSize).toBe(source.length) + expect(module!.virtual).toBe(false) + }) }) diff --git a/packages/vite/src/node/inspect/context.ts b/packages/vite/src/node/inspect/context.ts index 2110a7634..588e10b8f 100644 --- a/packages/vite/src/node/inspect/context.ts +++ b/packages/vite/src/node/inspect/context.ts @@ -32,6 +32,7 @@ import { getPluginDetails as getEnvironmentPluginDetails, getPluginMetrics as getEnvironmentPluginMetrics, } from './plugins' +import { trackTransformRequestId } from './server' import { createViteInspectStore } from './store' import { normalizeModuleId, @@ -215,7 +216,7 @@ export class ViteInspectEnvironmentContext { this.inspectContext.store.recordTransform(this.scope, id, publicModuleId, { ...info, plugin_id: pluginId, - }, preTransformCode, pluginCall) + }, preTransformCode, pluginCall, trackTransformRequestId(id)) } recordLoad( @@ -240,7 +241,7 @@ export class ViteInspectEnvironmentContext { this.inspectContext.store.recordLoad(this.scope, id, publicModuleId, { ...info, plugin_id: pluginId, - }, pluginCall) + }, pluginCall, trackTransformRequestId(id)) } recordLoadCall( @@ -262,7 +263,7 @@ export class ViteInspectEnvironmentContext { if (!pluginCall) return - this.inspectContext.store.recordPluginCall(this.scope, pluginCall) + this.inspectContext.store.recordPluginCall(this.scope, pluginCall, trackTransformRequestId(id)) } recordResolveId( diff --git a/packages/vite/src/node/inspect/module.ts b/packages/vite/src/node/inspect/module.ts index 4b1d45b2f..a55e67392 100644 --- a/packages/vite/src/node/inspect/module.ts +++ b/packages/vite/src/node/inspect/module.ts @@ -31,6 +31,13 @@ export async function getModulesList( const transformList = await ctx.inspectContext.store.getTransformList(ctx.scope) const resolveIdList = await ctx.inspectContext.store.getResolveIdList(ctx.scope) + const declinedLoadMetrics = await ctx.inspectContext.store.getDeclinedLoadMetrics(ctx.scope) + const declinedLoadMetricsById = new Map() + for (const load of declinedLoadMetrics) { + const plugins = declinedLoadMetricsById.get(load.publicModuleId) ?? [] + plugins.push({ name: load.pluginName, transform: load.totalTime }) + declinedLoadMetricsById.set(load.publicModuleId, plugins) + } const transformsById = transformList.reduce>((map, transform) => { const transforms = map[transform.moduleId] ||= [] @@ -63,7 +70,9 @@ export async function getModulesList( name: resolveId.name, resolveId: resolveId.end - resolveId.start, })) - const plugins = transformPlugins.concat(resolveIdPlugins) + const loadPlugins = declinedLoadMetricsById.get(ctx.getPublicModuleId(id)) ?? [] + totalTime += loadPlugins.reduce((sum, plugin) => sum + (plugin.transform ?? 0), 0) + const plugins = transformPlugins.concat(loadPlugins, resolveIdPlugins) const firstTransform = transforms[0] const lastTransform = transforms.at(-1) diff --git a/packages/vite/src/node/inspect/server.ts b/packages/vite/src/node/inspect/server.ts index a777945c5..b7aaaa29e 100644 --- a/packages/vite/src/node/inspect/server.ts +++ b/packages/vite/src/node/inspect/server.ts @@ -64,6 +64,8 @@ export function setupEnvironmentInvalidation(server: ViteDevServer, vite: ViteIn state.activeRequests.add(request) return transformRequestStorage.run(request, () => transformRequest.apply(this, args)) .finally(() => { + for (const context of state.contexts) + context.inspectContext.store.clearPendingLoadMetrics(context.scope, request) request.active = false request.ids.clear() state.activeRequests.delete(request) diff --git a/packages/vite/src/node/inspect/store/index.ts b/packages/vite/src/node/inspect/store/index.ts index 803050add..44d7780b0 100644 --- a/packages/vite/src/node/inspect/store/index.ts +++ b/packages/vite/src/node/inspect/store/index.ts @@ -12,6 +12,7 @@ import type { PendingWrite, QueuedWrite, StoredViteInspectTransformInfo, + ViteInspectDeclinedLoadMetric, ViteInspectPayloadRange, ViteInspectPluginMetricItem, ViteInspectResolveIdItem, @@ -31,6 +32,7 @@ const DEFAULT_MAX_BATCH_ITEMS = 256 const DEFAULT_MAX_BATCH_BYTES = 8 * 1024 * 1024 interface StoredModuleTransforms { + transformRequest?: object publicModuleId: string invokeCount: number items: StoredViteInspectTransformInfo[] @@ -39,10 +41,12 @@ interface StoredModuleTransforms { interface ScopeData { transforms: Map resolveIds: Map + declinedLoadMetrics: Map> + pendingLoadMetrics: Map>> } interface PreparedWrite { - write: Exclude + write: Exclude resultIndex?: number sourceIndex?: number sourcemapIndex?: number @@ -121,6 +125,7 @@ class PayloadViteInspectStore implements ViteInspectStore { info: ViteInspectTransformInfo, preTransformCode: string, pluginCall?: ViteInspectPluginCallInfo, + transformRequest?: object, ): void { this.enqueue({ operation: 'recordTransform', @@ -130,6 +135,7 @@ class PayloadViteInspectStore implements ViteInspectStore { info, preTransformCode, pluginCall, + transformRequest, }) } @@ -139,6 +145,7 @@ class PayloadViteInspectStore implements ViteInspectStore { publicModuleId: string, info: ViteInspectTransformInfo, pluginCall?: ViteInspectPluginCallInfo, + transformRequest?: object, ): void { this.enqueue({ operation: 'recordLoad', @@ -147,6 +154,7 @@ class PayloadViteInspectStore implements ViteInspectStore { publicModuleId, info, pluginCall, + transformRequest, }) } @@ -169,14 +177,19 @@ class PayloadViteInspectStore implements ViteInspectStore { }) } - recordPluginCall(scope: string, info: ViteInspectPluginCallInfo): void { + recordPluginCall(scope: string, info: ViteInspectPluginCallInfo, transformRequest?: object): void { this.enqueue({ operation: 'recordPluginCall', scope, info, + transformRequest, }) } + clearPendingLoadMetrics(scope: string, transformRequest: object): void { + this.enqueue({ operation: 'clearPendingLoadMetrics', scope, transformRequest }) + } + invalidate(scope: string, moduleId: string, publicModuleId: string): void { this.enqueue({ operation: 'invalidate', @@ -214,6 +227,13 @@ class PayloadViteInspectStore implements ViteInspectStore { return result } + async getDeclinedLoadMetrics(scope: string): Promise { + await this.flush() + return Array.from(this.scopes.get(scope)?.declinedLoadMetrics ?? []).flatMap(([publicModuleId, metrics]) => + Array.from(metrics.values(), metric => ({ ...metric, publicModuleId })), + ) + } + async getResolveIdList(scope: string): Promise { await this.flush() return Array.from(this.scopes.get(scope)?.resolveIds.values() ?? []).flat() @@ -401,7 +421,7 @@ class PayloadViteInspectStore implements ViteInspectStore { } private async writeBatch(batch: PendingWrite[]): Promise { - let dataWrites: Array> = [] + let dataWrites: Array> = [] const flushDataWrites = async () => { if (dataWrites.length === 0) return @@ -415,6 +435,10 @@ class PayloadViteInspectStore implements ViteInspectStore { await flushDataWrites() this.invalidateNow(write.scope, write.moduleId, write.publicModuleId) } + else if (write.operation === 'clearPendingLoadMetrics') { + await flushDataWrites() + this.scopes.get(write.scope)?.pendingLoadMetrics.delete(write.transformRequest) + } else if (write.operation === 'clearScope') { await flushDataWrites() this.clearScopeNow(write.scope) @@ -427,21 +451,25 @@ class PayloadViteInspectStore implements ViteInspectStore { } private async writeDataBatch( - writes: Array>, + writes: Array>, ): Promise { const values: Array = [] const prepared: PreparedWrite[] = [] const pluginCallWrites: InspectPluginCallWrite[] = [] - const sourceState = new Map() + const sourceState = new Map() - const hasSource = (scope: string, moduleId: string): boolean => { + const hasSource = (scope: string, moduleId: string, transformRequest?: object): boolean => { const key = scopeKey(scope, moduleId) - const pending = sourceState.get(key) - if (pending != null) - return pending - const stored = this.scopes.get(scope)?.transforms.get(moduleId)?.items.some(item => item.hasResult) ?? false - sourceState.set(key, stored) - return stored + let state = sourceState.get(key) + if (!state) { + const stored = this.scopes.get(scope)?.transforms.get(moduleId) + state = { + hasSource: stored?.items.some(item => item.hasResult) ?? false, + transformRequest: stored?.transformRequest, + } + sourceState.set(key, state) + } + return state.hasSource && (!transformRequest || state.transformRequest === transformRequest) } for (const write of writes) { @@ -452,15 +480,15 @@ class PayloadViteInspectStore implements ViteInspectStore { info: pluginCall, }) } - if (write.operation === 'recordPluginCall') + if (write.operation === 'recordPluginCall' && write.info.type !== 'load') continue const item: PreparedWrite = { write } if (write.operation === 'recordTransform') { const key = scopeKey(write.scope, write.moduleId) - if (!hasSource(write.scope, write.moduleId)) { + if (!hasSource(write.scope, write.moduleId, write.transformRequest)) { item.sourceIndex = values.push(write.preTransformCode) - 1 - sourceState.set(key, true) + sourceState.set(key, { hasSource: true, transformRequest: write.transformRequest }) } item.resultIndex = values.push(write.info.result) - 1 item.sourcemapIndex = pushSerializedPayload(values, write.info.sourcemaps) @@ -468,7 +496,7 @@ class PayloadViteInspectStore implements ViteInspectStore { else if (write.operation === 'recordLoad') { item.resultIndex = values.push(write.info.result) - 1 item.sourcemapIndex = pushSerializedPayload(values, write.info.sourcemaps) - sourceState.set(scopeKey(write.scope, write.moduleId), write.info.result != null) + sourceState.set(scopeKey(write.scope, write.moduleId), { hasSource: write.info.result != null, transformRequest: write.transformRequest }) } prepared.push(item) } @@ -501,6 +529,21 @@ class PayloadViteInspectStore implements ViteInspectStore { rangeAt(ranges, prepared.sourcemapIndex), ) } + else if (write.operation === 'recordPluginCall' && write.info.type === 'load') { + // Declined loads have timing data but must not replace the module's code. + const data = this.getScope(write.scope) + let pendingLoadMetrics = data.pendingLoadMetrics.get(write.transformRequest) + if (!pendingLoadMetrics) { + pendingLoadMetrics = new Map() + data.pendingLoadMetrics.set(write.transformRequest, pendingLoadMetrics) + } + let metrics = pendingLoadMetrics.get(write.info.module) + if (!metrics) { + metrics = new Map() + pendingLoadMetrics.set(write.info.module, metrics) + } + addMetric(metrics, write.info.plugin_id, write.info.plugin_name, write.info.duration) + } else if (write.operation === 'recordResolveId') { const data = this.getScope(write.scope) let resolveIds = data.resolveIds.get(write.sourceId) @@ -529,16 +572,23 @@ class PayloadViteInspectStore implements ViteInspectStore { ): void { const data = this.getScope(write.scope) let transforms = data.transforms.get(write.moduleId) - if (!transforms) { + // A new request can reach transform through Vite's direct file read. + // Replace the previous snapshot even when it contains a load error result. + if (!transforms || (write.transformRequest && transforms.transformRequest !== write.transformRequest)) { + if (transforms) + this.payloads.reclaim(collectPayloadRanges(transforms.items)) transforms = { + transformRequest: write.transformRequest, publicModuleId: write.publicModuleId, - invokeCount: 0, + invokeCount: transforms?.invokeCount ?? 0, items: [], } data.transforms.set(write.moduleId, transforms) } if (!transforms.items.some(item => item.hasResult)) { + // Vite can read files itself without a successful load hook. + this.commitDeclinedLoadMetrics(data, write.publicModuleId, write.transformRequest) transforms.items.push({ name: DUMMY_LOAD_PLUGIN_NAME, hasResult: true, @@ -559,16 +609,31 @@ class PayloadViteInspectStore implements ViteInspectStore { sourcemaps: ViteInspectPayloadRange | undefined, ): void { const data = this.getScope(write.scope) + this.commitDeclinedLoadMetrics(data, write.publicModuleId, write.transformRequest) const existing = data.transforms.get(write.moduleId) if (existing) this.payloads.reclaim(collectPayloadRanges(existing.items)) data.transforms.set(write.moduleId, { + transformRequest: write.transformRequest, publicModuleId: write.publicModuleId, invokeCount: (existing?.invokeCount ?? 0) + 1, items: [toStoredTransformInfo(write.info, result, sourcemaps)], }) } + private commitDeclinedLoadMetrics(data: ScopeData, publicModuleId: string, transformRequest?: object): void { + // Only this request can contribute declined loads to its code snapshot. + const pendingLoadMetrics = data.pendingLoadMetrics.get(transformRequest) + const declinedLoadMetrics = pendingLoadMetrics?.get(publicModuleId) + if (declinedLoadMetrics) + data.declinedLoadMetrics.set(publicModuleId, declinedLoadMetrics) + else + data.declinedLoadMetrics.delete(publicModuleId) + pendingLoadMetrics?.delete(publicModuleId) + if (pendingLoadMetrics?.size === 0) + data.pendingLoadMetrics.delete(transformRequest) + } + private invalidateNow(scope: string, moduleId: string, publicModuleId: string): void { const data = this.scopes.get(scope) if (!data) { @@ -617,6 +682,15 @@ class PayloadViteInspectStore implements ViteInspectStore { data.resolveIds.delete(sourceId) } + for (const id of invalidPublicIds) { + data.declinedLoadMetrics.delete(id) + for (const [request, pendingLoadMetrics] of data.pendingLoadMetrics) { + pendingLoadMetrics.delete(id) + if (pendingLoadMetrics.size === 0) + data.pendingLoadMetrics.delete(request) + } + } + this.pluginCalls.invalidate(scope, invalidPublicIds) } @@ -680,6 +754,8 @@ class PayloadViteInspectStore implements ViteInspectStore { data = { transforms: new Map(), resolveIds: new Map(), + declinedLoadMetrics: new Map(), + pendingLoadMetrics: new Map(), } this.scopes.set(scope, data) } @@ -852,7 +928,7 @@ function positiveInteger(value: number | undefined, fallback: number): number { } function estimateWriteBytes(write: QueuedWrite): number { - const pluginCall = write.operation === 'invalidate' || write.operation === 'clearScope' + const pluginCall = write.operation === 'invalidate' || write.operation === 'clearScope' || write.operation === 'clearPendingLoadMetrics' ? undefined : getQueuedPluginCall(write) const pluginCallBytes = pluginCall @@ -876,7 +952,7 @@ function estimateWriteBytes(write: QueuedWrite): number { } function getQueuedPluginCall( - write: Exclude, + write: Exclude, ): ViteInspectPluginCallInfo | undefined { return write.operation === 'recordPluginCall' ? write.info : write.pluginCall } diff --git a/packages/vite/src/node/inspect/store/types.ts b/packages/vite/src/node/inspect/store/types.ts index d1d8d6493..e66e452a9 100644 --- a/packages/vite/src/node/inspect/store/types.ts +++ b/packages/vite/src/node/inspect/store/types.ts @@ -70,6 +70,10 @@ export interface ViteInspectPluginMetricItem { totalTime: number } +export interface ViteInspectDeclinedLoadMetric extends ViteInspectPluginMetricItem { + publicModuleId: string +} + export interface ViteInspectStore { recordTransform: ( scope: string, @@ -78,6 +82,7 @@ export interface ViteInspectStore { info: ViteInspectTransformInfo, preTransformCode: string, pluginCall?: ViteInspectPluginCallInfo, + transformRequest?: object, ) => void recordLoad: ( scope: string, @@ -85,6 +90,7 @@ export interface ViteInspectStore { publicModuleId: string, info: ViteInspectTransformInfo, pluginCall?: ViteInspectPluginCallInfo, + transformRequest?: object, ) => void recordResolveId: ( scope: string, @@ -97,10 +103,13 @@ export interface ViteInspectStore { recordPluginCall: ( scope: string, info: ViteInspectPluginCallInfo, + transformRequest?: object, ) => void + clearPendingLoadMetrics: (scope: string, transformRequest: object) => void invalidate: (scope: string, moduleId: string, publicModuleId: string) => void clearScope: (scope: string) => void getTransformList: (scope: string) => Promise + getDeclinedLoadMetrics: (scope: string) => Promise getResolveIdList: (scope: string) => Promise getPluginTransformMetrics: (scope: string) => Promise getPluginResolveIdMetrics: (scope: string) => Promise @@ -123,6 +132,7 @@ export type QueuedWrite info: ViteInspectTransformInfo preTransformCode: string pluginCall?: ViteInspectPluginCallInfo + transformRequest?: object } | { operation: 'recordLoad' @@ -131,6 +141,7 @@ export type QueuedWrite publicModuleId: string info: ViteInspectTransformInfo pluginCall?: ViteInspectPluginCallInfo + transformRequest?: object } | { operation: 'recordResolveId' @@ -145,6 +156,12 @@ export type QueuedWrite operation: 'recordPluginCall' scope: string info: ViteInspectPluginCallInfo + transformRequest?: object + } + | { + operation: 'clearPendingLoadMetrics' + scope: string + transformRequest: object } | { operation: 'invalidate'