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
126 changes: 126 additions & 0 deletions packages/vite/src/node/__tests__/inspect-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
})
})
153 changes: 153 additions & 0 deletions packages/vite/src/node/__tests__/inspect-server.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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(
Expand Down Expand Up @@ -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)
})
})
7 changes: 4 additions & 3 deletions packages/vite/src/node/inspect/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
getPluginDetails as getEnvironmentPluginDetails,
getPluginMetrics as getEnvironmentPluginMetrics,
} from './plugins'
import { trackTransformRequestId } from './server'
import { createViteInspectStore } from './store'
import {
normalizeModuleId,
Expand Down Expand Up @@ -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(
Expand All @@ -240,7 +241,7 @@ export class ViteInspectEnvironmentContext {
this.inspectContext.store.recordLoad(this.scope, id, publicModuleId, {
...info,
plugin_id: pluginId,
}, pluginCall)
}, pluginCall, trackTransformRequestId(id))
}

recordLoadCall(
Expand All @@ -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(
Expand Down
11 changes: 10 additions & 1 deletion packages/vite/src/node/inspect/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ViteInspectModulePluginMetric[]>()
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<Record<string, typeof transformList>>((map, transform) => {
const transforms = map[transform.moduleId] ||= []
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions packages/vite/src/node/inspect/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading