mirror of https://github.com/vuejs/vitepress
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
6.9 KiB
229 lines
6.9 KiB
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads'
|
|
import RPCContext, {
|
|
deferPromise,
|
|
type RPCContextOptions
|
|
} from 'rpc-magic-proxy'
|
|
import { task, updateCurrentTask } from './utils/task'
|
|
import c from 'picocolors'
|
|
import Queue from './utils/queue'
|
|
import _debug from 'debug'
|
|
import type { SiteConfig } from 'siteConfig'
|
|
import humanizeDuration from 'humanize-duration'
|
|
|
|
export type SupportsParallel = 'render' | 'local-search'
|
|
|
|
const options: RPCContextOptions = {
|
|
carryThis: false,
|
|
carrySideEffect: false
|
|
}
|
|
/**
|
|
* Checks if the given task should be run in parallel.
|
|
* If task is omitted, checks if any task should be run in parallel.
|
|
*/
|
|
export function shouldUseParallel(config: SiteConfig, task?: SupportsParallel) {
|
|
const { parallel = false } = config
|
|
if (task === undefined)
|
|
return parallel === true || (Array.isArray(parallel) && parallel.length > 0)
|
|
if (typeof parallel === 'boolean') return parallel
|
|
if (Array.isArray(parallel)) return parallel.includes(task)
|
|
throw new TypeError(`Invalid value for config.parallel: ${parallel}`)
|
|
}
|
|
|
|
let debug = _debug('vitepress:worker:main')
|
|
const WORKER_MAGIC = 'vitepress:worker'
|
|
/*=============================== Main Thread ===============================*/
|
|
interface WorkerTask {
|
|
name: string
|
|
argv: any[]
|
|
resolve: (retVal: any) => void
|
|
reject: (error?: any) => void
|
|
}
|
|
|
|
// Owned by main thread, will be distributed to workers
|
|
let taskQueue: Queue<WorkerTask> | null = null
|
|
|
|
function dispatchWork(name: string, ...argv: any[]): Promise<any> {
|
|
if (workerMeta) {
|
|
return workerMeta.dispatchWork(name, ...argv)
|
|
} else if (taskQueue) {
|
|
const { promise, resolve, reject } = deferPromise()
|
|
taskQueue.enqueue({ name, argv, resolve, reject })
|
|
return promise
|
|
} else {
|
|
throw new Error(`trying to dispatch ${name} before launching workers.`)
|
|
}
|
|
}
|
|
|
|
type WorkerInstance = Worker & {
|
|
workerId: string
|
|
hooks: {
|
|
// Update worker's context
|
|
updateContext: (ctx: Object | null) => void
|
|
}
|
|
}
|
|
|
|
const workers: Array<WorkerInstance> = []
|
|
|
|
export async function launchWorkers(numWorkers: number, context: Object) {
|
|
debug(`launching ${numWorkers} workers`)
|
|
taskQueue = new Queue<WorkerTask>()
|
|
const allInitialized: Array<Promise<void>> = []
|
|
const ctx = new RPCContext(options)
|
|
const getNextTask = () => taskQueue?.dequeue() ?? null
|
|
for (let i = 0; i < numWorkers; i++) {
|
|
const workerId = (i + 1).toString().padStart(2, '0')
|
|
const { promise, resolve } = deferPromise<void>()
|
|
const initWorkerHooks = (hooks: WorkerInstance['hooks']) => {
|
|
Object.assign(worker, { workerId, hooks })
|
|
resolve()
|
|
}
|
|
const debug = _debug(`vitepress:worker:${workerId.padEnd(4)}`)
|
|
const payload = await ctx.serialize({
|
|
workerMeta: {
|
|
workerId,
|
|
dispatchWork,
|
|
// Save some RPC overhead when debugger is not active
|
|
debug: debug.enabled ? debug : null,
|
|
task,
|
|
updateCurrentTask
|
|
},
|
|
initWorkerHooks,
|
|
getNextTask,
|
|
context
|
|
})
|
|
const worker = new Worker(new URL(import.meta.url), {
|
|
workerData: { [WORKER_MAGIC]: payload }
|
|
}) as WorkerInstance
|
|
ctx.bind(worker as any)
|
|
workers.push(worker)
|
|
allInitialized.push(promise)
|
|
worker.on('error', console.error)
|
|
}
|
|
await Promise.all(allInitialized)
|
|
}
|
|
|
|
export function updateContext(context: Object) {
|
|
return Promise.all(workers.map(({ hooks }) => hooks.updateContext(context)))
|
|
}
|
|
|
|
// Wait for workers to finish and exit.
|
|
// Will return immediately if no worker exists.
|
|
export async function stopWorkers(reason: string = 'exit') {
|
|
debug('stopping workers:', reason)
|
|
const allClosed = workers.map((w) =>
|
|
new Promise<void>((res) => w.once('exit', () => res())).then(() =>
|
|
debug(`worker:${w.workerId} confirmed exit`)
|
|
)
|
|
)
|
|
taskQueue?.close()
|
|
taskQueue = null
|
|
const success = await Promise.any([
|
|
Promise.all(allClosed).then(() => true),
|
|
new Promise<false>((res) => setTimeout(() => res(false), 1500))
|
|
])
|
|
if (!success) {
|
|
debug('forcefully terminating workers')
|
|
for (const w of workers) {
|
|
try {
|
|
w.terminate()
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*============================== Worker Thread ==============================*/
|
|
|
|
export let workerMeta: {
|
|
workerId: string
|
|
dispatchWork: typeof dispatchWork
|
|
debug: typeof debug
|
|
task: typeof task
|
|
updateCurrentTask: typeof updateCurrentTask
|
|
} | null = null
|
|
|
|
const registry: Map<string, { main: Function; init?: Function }> = new Map()
|
|
|
|
export function registerWorkload<T extends Object, K extends any[], V>(
|
|
name: string,
|
|
main: (this: T, ...args: K) => V,
|
|
init?: (this: T, ...args: void[]) => void
|
|
) {
|
|
// Only register workload in worker threads
|
|
if (!isMainThread) {
|
|
if (registry.has(name))
|
|
throw new Error(`Workload "${name}" already registered.`)
|
|
registry.set(name, { main, init })
|
|
}
|
|
return (...args: Parameters<typeof main>) =>
|
|
dispatchWork(name, ...args) as Promise<Awaited<ReturnType<typeof main>>>
|
|
}
|
|
|
|
// Will keep querying next workload from main thread
|
|
async function workerMainLoop() {
|
|
const ctx = new RPCContext(options).bind(parentPort! as any)
|
|
const {
|
|
workerMeta: _workerMeta,
|
|
initWorkerHooks,
|
|
getNextTask,
|
|
context
|
|
}: {
|
|
workerMeta: typeof workerMeta
|
|
getNextTask: () => Promise<WorkerTask | null>
|
|
initWorkerHooks: (hooks: Object) => Promise<void>
|
|
context: Object
|
|
} = ctx.deserialize(workerData[WORKER_MAGIC]) as any
|
|
// Set up magic proxy to main thread dispatchWork
|
|
workerMeta = _workerMeta!
|
|
if (workerMeta.debug) debug = workerMeta.debug
|
|
else debug = (() => {}) as any as typeof debug
|
|
debug(`started`)
|
|
// Upon worker initialization, report back the hooks that main thread can use
|
|
// to reach this worker.
|
|
await initWorkerHooks({
|
|
updateContext(ctx: Object | null) {
|
|
if (ctx === null) for (const k in context) delete (context as any)[k]
|
|
else Object.assign(context, ctx)
|
|
}
|
|
})
|
|
|
|
let workTime = 0
|
|
while (true) {
|
|
const task = await getNextTask()
|
|
if (task === null) break
|
|
const { name, argv, resolve, reject } = task
|
|
if (!registry.has(name)) throw new Error(`No task "${name}" registered.`)
|
|
const el = registry.get(name)!
|
|
const { main, init } = el
|
|
const timeStart = performance.now()
|
|
if (init) {
|
|
try {
|
|
await init.apply(context)
|
|
} catch (e) {
|
|
console.error(c.red(`worker: failed to init workload "${name}":`), e)
|
|
reject(e)
|
|
} finally {
|
|
el.init = undefined
|
|
}
|
|
}
|
|
try {
|
|
resolve(await main.apply(context, argv))
|
|
} catch (e) {
|
|
console.error(
|
|
c.red(`worker:${workerMeta.workerId} error running task "${name}":`),
|
|
e
|
|
)
|
|
reject(e)
|
|
} finally {
|
|
workTime += performance.now() - timeStart
|
|
}
|
|
}
|
|
ctx.reset()
|
|
const duration = humanizeDuration(workTime, {
|
|
maxDecimalPoints: 2
|
|
})
|
|
await debug(`stopped - total workload: ${duration}`)
|
|
}
|
|
|
|
if (!isMainThread && workerData?.[WORKER_MAGIC])
|
|
workerMainLoop().then(() => process.exit())
|