mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
perf(hmr): poll client bundles only
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* HMR plugin, node half: the host end of the dev reload chain. One interval
|
||||
* stat-polls every graph row's client bundle and optional source map (polling
|
||||
* by design: network mounts deliver no inotify events), reports changes through
|
||||
* stat-polls every graph row's client bundle (polling by design: network mounts
|
||||
* deliver no inotify events), reports changes through
|
||||
* `clientModuleHost.rebuilt(id)`, and serves the `/plugins/events` SSE channel
|
||||
* broadcasting graph/rebuilt frames to the browser half (src/client/).
|
||||
* The web bundle mounts this row unconditionally: without a rebuild
|
||||
@@ -42,30 +42,22 @@ function sseData(frame: PluginsEventFrame): string {
|
||||
return `data: ${JSON.stringify(frame)}\n\n`
|
||||
}
|
||||
|
||||
type WatchedArtifactStat = Omit<ClientArtifactBaseline, 'path'>
|
||||
type WatchedBundleStat = Omit<ClientArtifactBaseline, 'path'>
|
||||
|
||||
type WatchedBundle = {
|
||||
-readonly [K in keyof ClientArtifactBaseline]: ClientArtifactBaseline[K]
|
||||
} & { dirty: boolean }
|
||||
|
||||
/** Snapshot the bundle plus its optional development source map. */
|
||||
function artifactStat(path: string): WatchedArtifactStat {
|
||||
/** Snapshot the executable bundle metadata that drives reloads. */
|
||||
function bundleStat(path: string): WatchedBundleStat {
|
||||
const bundle = statSync(path)
|
||||
try {
|
||||
const map = statSync(`${path}.map`)
|
||||
return { mtimeMs: bundle.mtimeMs, size: bundle.size, mapMtimeMs: map.mtimeMs, mapSize: map.size }
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
return { mtimeMs: bundle.mtimeMs, size: bundle.size, mapMtimeMs: null, mapSize: null }
|
||||
}
|
||||
return { mtimeMs: bundle.mtimeMs, size: bundle.size }
|
||||
}
|
||||
|
||||
/** Whether neither served artifact changed since the last successful re-hash. */
|
||||
function sameArtifactStat(left: WatchedArtifactStat, right: WatchedArtifactStat): boolean {
|
||||
/** Whether the executable bundle is unchanged since the last successful re-hash. */
|
||||
function sameBundleStat(left: WatchedBundleStat, right: WatchedBundleStat): boolean {
|
||||
return left.mtimeMs === right.mtimeMs
|
||||
&& left.size === right.size
|
||||
&& left.mapMtimeMs === right.mapMtimeMs
|
||||
&& left.mapSize === right.mapSize
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,7 +72,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
// --- bundle watch: one HMR-owned stat poll ------------------------------
|
||||
const watched = new Map<string, WatchedBundle>()
|
||||
|
||||
const rehash = (id: string, watch: WatchedBundle, current: WatchedArtifactStat): void => {
|
||||
const rehash = (id: string, watch: WatchedBundle, current: WatchedBundleStat): void => {
|
||||
try {
|
||||
// rebuilt() replaces the opaque startup rev on its first call; later
|
||||
// calls stay silent when the content hash is unchanged.
|
||||
@@ -95,17 +87,15 @@ export function apply(ctx: Context, config: Config): void {
|
||||
}
|
||||
watch.mtimeMs = current.mtimeMs
|
||||
watch.size = current.size
|
||||
watch.mapMtimeMs = current.mapMtimeMs
|
||||
watch.mapSize = current.mapSize
|
||||
watch.dirty = false
|
||||
}
|
||||
|
||||
const watchRow = (id: string, baseline: ClientArtifactBaseline): void => {
|
||||
const watch: WatchedBundle = { ...baseline, dirty: false }
|
||||
watched.set(id, watch)
|
||||
let current: WatchedArtifactStat
|
||||
let current: WatchedBundleStat
|
||||
try {
|
||||
current = artifactStat(baseline.path)
|
||||
current = bundleStat(baseline.path)
|
||||
} catch (error) {
|
||||
watch.dirty = true
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') ctx.logger.warn(error)
|
||||
@@ -113,20 +103,20 @@ export function apply(ctx: Context, config: Config): void {
|
||||
}
|
||||
// The module host captured its baseline before reading the bytes in the
|
||||
// startup batch. Only a mismatch crosses into the content-hash path.
|
||||
if (!sameArtifactStat(current, watch)) rehash(id, watch, current)
|
||||
if (!sameBundleStat(current, watch)) rehash(id, watch, current)
|
||||
}
|
||||
|
||||
const pollWatches = (): void => {
|
||||
for (const [id, watch] of watched) {
|
||||
let current: WatchedArtifactStat
|
||||
let current: WatchedBundleStat
|
||||
try {
|
||||
current = artifactStat(watch.path)
|
||||
current = bundleStat(watch.path)
|
||||
} catch (error) {
|
||||
watch.dirty = true
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') ctx.logger.warn(error)
|
||||
continue
|
||||
}
|
||||
if (!watch.dirty && sameArtifactStat(current, watch)) continue
|
||||
if (!watch.dirty && sameBundleStat(current, watch)) continue
|
||||
// Stat-before-hash preserves a detectable older baseline for writes that
|
||||
// land during hashing. Repeated stat changes heal a torn read.
|
||||
rehash(id, watch, current)
|
||||
|
||||
@@ -31,19 +31,7 @@ interface FakeHostOptions {
|
||||
|
||||
function artifactBaseline(path: string): ClientArtifactBaseline {
|
||||
const bundle = statSync(path)
|
||||
try {
|
||||
const sourceMap = statSync(`${path}.map`)
|
||||
return {
|
||||
path,
|
||||
mtimeMs: bundle.mtimeMs,
|
||||
size: bundle.size,
|
||||
mapMtimeMs: sourceMap.mtimeMs,
|
||||
mapSize: sourceMap.size,
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
return { path, mtimeMs: bundle.mtimeMs, size: bundle.size, mapMtimeMs: null, mapSize: null }
|
||||
}
|
||||
return { path, mtimeMs: bundle.mtimeMs, size: bundle.size }
|
||||
}
|
||||
|
||||
function fakeClientModuleHost(rows: Map<string, string>, options: FakeHostOptions = {}): FakeHost {
|
||||
@@ -111,7 +99,7 @@ async function mount(clientModuleHost: FakeHost, webServer: WebServer) {
|
||||
}
|
||||
|
||||
describe('hmr node half', () => {
|
||||
it('watches graph bundles, reports stat changes, and unwatches on dispose', async () => {
|
||||
it('watches graph bundles, ignores map-only changes, and unwatches on dispose', async () => {
|
||||
const bundle = join(dir, 'a.js')
|
||||
writeFileSync(bundle, 'v1')
|
||||
const clientModuleHost = fakeClientModuleHost(new Map([['pkg-a', bundle]]))
|
||||
@@ -130,13 +118,17 @@ describe('hmr node half', () => {
|
||||
clientModuleHost.rebuiltCalls.length = 0
|
||||
await new Promise(resolve => setTimeout(resolve, POLL_MS * 2))
|
||||
writeFileSync(`${bundle}.map`, '{"version":3}')
|
||||
await new Promise(resolve => setTimeout(resolve, POLL_MS * 3))
|
||||
expect(clientModuleHost.rebuiltCalls).toEqual([])
|
||||
|
||||
writeFileSync(bundle, 'v3-even-longer')
|
||||
await vi.waitFor(() => { expect(clientModuleHost.rebuiltCalls).toContain('pkg-a') }, { timeout: 3_000 })
|
||||
|
||||
await fiber.dispose()
|
||||
expect(routes).toHaveLength(0)
|
||||
// Watcher gone: further file changes report nothing.
|
||||
clientModuleHost.rebuiltCalls.length = 0
|
||||
writeFileSync(bundle, 'v3-even-longer')
|
||||
writeFileSync(bundle, 'v4-after-dispose')
|
||||
await new Promise(resolve => setTimeout(resolve, POLL_MS * 4))
|
||||
expect(clientModuleHost.rebuiltCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
import { createHash, randomBytes } from 'node:crypto'
|
||||
import { readFileSync, statSync, type Stats } from 'node:fs'
|
||||
import { readFileSync, statSync } from 'node:fs'
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
@@ -78,10 +78,6 @@ export interface ClientArtifactBaseline {
|
||||
readonly mtimeMs: number
|
||||
/** Bundle size in bytes. */
|
||||
readonly size: number
|
||||
/** Source-map modification time, or null when no map was observable. */
|
||||
readonly mapMtimeMs: number | null
|
||||
/** Source-map size in bytes, or null when no map was observable. */
|
||||
readonly mapSize: number | null
|
||||
}
|
||||
|
||||
/** Resolved package metadata for one `dsh.client` package (cached per name, never expires). */
|
||||
@@ -756,22 +752,13 @@ export class ClientModuleRegistry extends Service {
|
||||
return meta
|
||||
}
|
||||
|
||||
/** Capture the bundle and optional-map stats before reading their bytes. */
|
||||
/** Capture the bundle stats before reading its bytes. */
|
||||
private captureArtifactBaseline(clientPath: string): ClientArtifactBaseline {
|
||||
const bundle = statSync(clientPath)
|
||||
let sourceMap: Stats | undefined
|
||||
try {
|
||||
sourceMap = statSync(`${clientPath}.map`)
|
||||
} catch {
|
||||
// Optional map metadata only seeds HMR; the following map read reports
|
||||
// malformed or inaccessible bytes and a later stat change self-heals.
|
||||
}
|
||||
return {
|
||||
path: clientPath,
|
||||
mtimeMs: bundle.mtimeMs,
|
||||
size: bundle.size,
|
||||
mapMtimeMs: sourceMap?.mtimeMs ?? null,
|
||||
mapSize: sourceMap?.size ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -378,8 +378,6 @@ describe('client bundle activation', () => {
|
||||
path: firstPath,
|
||||
mtimeMs: firstStat.mtimeMs,
|
||||
size: firstStat.size,
|
||||
mapMtimeMs: null,
|
||||
mapSize: null,
|
||||
})
|
||||
expect(service.artifactBaseline('@fixture/unknown')).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -3434,7 +3434,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'ClientArtifactBaseline',
|
||||
declaration: 'export interface ClientArtifactBaseline {\n readonly path: string;\n readonly mtimeMs: number;\n readonly size: number;\n readonly mapMtimeMs: number | null;\n readonly mapSize: number | null;\n}',
|
||||
declaration: 'export interface ClientArtifactBaseline {\n readonly path: string;\n readonly mtimeMs: number;\n readonly size: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'CodeBindingErrorClass',
|
||||
|
||||
Reference in New Issue
Block a user