siteConfig: provide better control over parallelism, disable parallel bundle by default

pull/3386/head
Yuxuan Zhang 2 years ago
parent 8edb0763d1
commit 346e54d57a
No known key found for this signature in database
GPG Key ID: 6910B04F3351EF7D

@ -193,7 +193,7 @@
"rollup": "^4.9.2", "rollup": "^4.9.2",
"rollup-plugin-dts": "^6.1.0", "rollup-plugin-dts": "^6.1.0",
"rollup-plugin-esbuild": "^6.1.0", "rollup-plugin-esbuild": "^6.1.0",
"rpc-magic-proxy": "^1.0.5", "rpc-magic-proxy": "^1.0.6",
"semver": "^7.5.4", "semver": "^7.5.4",
"simple-git-hooks": "^2.9.0", "simple-git-hooks": "^2.9.0",
"sirv": "^2.0.4", "sirv": "^2.0.4",

@ -280,8 +280,8 @@ importers:
specifier: ^6.1.0 specifier: ^6.1.0
version: 6.1.0(esbuild@0.19.11)(rollup@4.9.2)(supports-color@9.4.0) version: 6.1.0(esbuild@0.19.11)(rollup@4.9.2)(supports-color@9.4.0)
rpc-magic-proxy: rpc-magic-proxy:
specifier: ^1.0.5 specifier: ^1.0.6
version: 1.0.5 version: 1.0.6
semver: semver:
specifier: ^7.5.4 specifier: ^7.5.4
version: 7.5.4 version: 7.5.4
@ -4016,8 +4016,8 @@ packages:
'@rollup/rollup-win32-x64-msvc': 4.9.2 '@rollup/rollup-win32-x64-msvc': 4.9.2
fsevents: 2.3.3 fsevents: 2.3.3
/rpc-magic-proxy@1.0.5: /rpc-magic-proxy@1.0.6:
resolution: {integrity: sha512-fd9jbcTrde8F1Nb8+dbl17CYbtBOvzxgnZzYBZ8wV+hwEOZDFPiWvl3yfnJjnYXHmMNSadFkLAa/L9lUA4f8eQ==} resolution: {integrity: sha512-0h0zldD09BLpMi/46xuGFsre2h68LVhIIEqjrSq0bAyeP+ph4/BDmoG4ykNFrU7v2Bx4mS3vX/KNZPVlG6JXQQ==}
dev: true dev: true
/rrweb-cssom@0.6.0: /rrweb-cssom@0.6.0:

@ -16,10 +16,8 @@ import { bundle } from './bundle'
import { generateSitemap } from './generateSitemap' import { generateSitemap } from './generateSitemap'
import { renderPage, type RenderPageContext } from './render' import { renderPage, type RenderPageContext } from './render'
import humanizeDuration from 'humanize-duration' import humanizeDuration from 'humanize-duration'
import { launchWorkers, stopWorkers } from '../worker' import { launchWorkers, shouldUseParallel, stopWorkers } from '../worker'
import { registerWorkload, updateContext } from '../worker' import { registerWorkload, updateContext } from '../worker'
import { createMarkdownRenderer } from '../markdown/markdown'
import type { DefaultTheme } from '../shared'
type RenderFn = (path: string) => Promise<SSGContext> type RenderFn = (path: string) => Promise<SSGContext>
@ -53,25 +51,7 @@ export async function build(
const siteConfig = await resolveConfig(root, 'build', 'production') const siteConfig = await resolveConfig(root, 'build', 'production')
const unlinkVue = linkVue() const unlinkVue = linkVue()
if (siteConfig.parallel) { if (shouldUseParallel(siteConfig)) {
// Dirty fix: md.render() has side effects on env.
// When user provides a custom render function, it will be invoked in the
// main thread, but md.render will be called in the worker thread. The side
// effects on env will be lost. So we need to make _render a curry function,
// and use `md` provided in the main thread
const config = siteConfig as SiteConfig<DefaultTheme.Config>
const search = config.site?.themeConfig?.search
if (search?.provider === 'local' && search?.options?._render) {
const md = await createMarkdownRenderer(
config.srcDir,
config.markdown,
config.site.base,
config.logger
)
const _render = search.options._render
search.options._render = (src, env, _) => _render(src, env, md)
}
await launchWorkers(siteConfig.concurrency, { await launchWorkers(siteConfig.concurrency, {
config: siteConfig, config: siteConfig,
options: buildOptions options: buildOptions
@ -175,7 +155,7 @@ export async function build(
let task: (page: string) => Promise<void> let task: (page: string) => Promise<void>
if (siteConfig.parallel) { if (shouldUseParallel(siteConfig, 'render')) {
const { config, ...additionalContext } = context const { config, ...additionalContext } = context
await updateContext({ renderEntry, ...additionalContext }) await updateContext({ renderEntry, ...additionalContext })
task = (page) => dispatchRenderPageWork(page) task = (page) => dispatchRenderPageWork(page)
@ -209,9 +189,7 @@ export async function build(
await generateSitemap(siteConfig) await generateSitemap(siteConfig)
await siteConfig.buildEnd?.(siteConfig) await siteConfig.buildEnd?.(siteConfig)
clearCache() clearCache()
await stopWorkers('build complete')
if (siteConfig.parallel) await stopWorkers('build finish')
const timeEnd = performance.now() const timeEnd = performance.now()
const duration = humanizeDuration(timeEnd - timeStart, { const duration = humanizeDuration(timeEnd - timeStart, {
maxDecimalPoints: 2 maxDecimalPoints: 2

@ -1,12 +1,13 @@
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import { build, type BuildOptions, type Rollup } from 'vite' import { build, type BuildOptions, type PluginOption, type Rollup } from 'vite'
import type { SiteConfig } from '../config' import type { SiteConfig } from '../config'
import { updateCurrentTask } from '../utils/task' import { updateCurrentTask } from '../utils/task'
import { buildMPAClient } from './buildMPAClient' import { buildMPAClient } from './buildMPAClient'
import { registerWorkload } from '../worker' import { registerWorkload, shouldUseParallel } from '../worker'
import resolveViteConfig from './viteConfig' import resolveViteConfig from './viteConfig'
import { type WorkerContext } from './build' import { type WorkerContext } from './build'
import { createVitePressPlugin } from '../plugin'
const dispatchBundleWorkload = registerWorkload( const dispatchBundleWorkload = registerWorkload(
'build:bundle', 'build:bundle',
@ -18,18 +19,18 @@ const dispatchBundleWorkload = registerWorkload(
} }
) )
async function bundleWorkload(this: WorkerContext, ssr: boolean) { async function bundleWorkload(
const pageToHashMap = Object.create(null) as Record<string, string> this: WorkerContext,
const clientJSMap = Object.create(null) as Record<string, string> ssr: boolean,
const result = (await build( plugins: PluginOption[]
) {
return build(
await resolveViteConfig(ssr, { await resolveViteConfig(ssr, {
config: this.config, config: this.config,
options: this.options, options: this.options,
pageToHashMap, plugins
clientJSMap
}) })
)) as Rollup.RollupOutput ) as Promise<Rollup.RollupOutput>
return { result, pageToHashMap, clientJSMap }
} }
async function bundleMPA( async function bundleMPA(
@ -75,27 +76,26 @@ export async function bundle(
const pageToHashMap = Object.create(null) const pageToHashMap = Object.create(null)
const clientJSMap = Object.create(null) const clientJSMap = Object.create(null)
const [server, client] = await Promise.all( const [serverResult, clientResult] = await Promise.all(
config.parallel [true, false].map(async (ssr) => {
? [ if (!ssr && config.mpa) return null
dispatchBundleWorkload(true), const plugins = await createVitePressPlugin(
config.mpa ? null : dispatchBundleWorkload(false) config,
] ssr,
: [ pageToHashMap,
bundleWorkload.apply({ config, options }, [true]), clientJSMap
config.mpa ? null : bundleWorkload.apply({ config, options }, [false]) )
] return shouldUseParallel(config, 'bundle')
? dispatchBundleWorkload(ssr, plugins)
: bundleWorkload.apply({ config, options }, [ssr, plugins])
})
) )
// Update maps
Object.assign(pageToHashMap, server.pageToHashMap, client?.pageToHashMap)
Object.assign(clientJSMap, server.clientJSMap, client?.clientJSMap)
return { return {
clientResult: config.mpa clientResult: config.mpa
? await bundleMPA(config, server.result, clientJSMap) ? await bundleMPA(config, serverResult!, clientJSMap)
: client?.result!, : clientResult!,
serverResult: server.result, serverResult: serverResult!,
pageToHashMap pageToHashMap
} }
} }

@ -4,12 +4,12 @@ import {
normalizePath, normalizePath,
type BuildOptions, type BuildOptions,
type Rollup, type Rollup,
type InlineConfig as ViteInlineConfig type InlineConfig as ViteInlineConfig,
type PluginOption
} from 'vite' } from 'vite'
import { APP_PATH } from '../alias' import { APP_PATH } from '../alias'
import type { SiteConfig } from '../config' import type { SiteConfig } from '../config'
import { slash } from '../shared' import { slash } from '../shared'
import { createVitePressPlugin } from '../plugin'
import { escapeRegExp, sanitizeFileName } from '../shared' import { escapeRegExp, sanitizeFileName } from '../shared'
// https://github.com/vitejs/vite/blob/d2aa0969ee316000d3b957d7e879f001e85e369e/packages/vite/src/node/plugins/splitVendorChunk.ts#L14 // https://github.com/vitejs/vite/blob/d2aa0969ee316000d3b957d7e879f001e85e369e/packages/vite/src/node/plugins/splitVendorChunk.ts#L14
@ -114,13 +114,11 @@ export default async function resolveViteConfig(
{ {
config, config,
options, options,
pageToHashMap, plugins
clientJSMap
}: { }: {
config: SiteConfig config: SiteConfig
options: BuildOptions options: BuildOptions
pageToHashMap: Record<string, string> plugins: PluginOption[]
clientJSMap: Record<string, string>
} }
): Promise<ViteInlineConfig> { ): Promise<ViteInlineConfig> {
return { return {
@ -128,12 +126,7 @@ export default async function resolveViteConfig(
cacheDir: config.cacheDir, cacheDir: config.cacheDir,
base: config.site.base, base: config.site.base,
logLevel: config.vite?.logLevel ?? 'warn', logLevel: config.vite?.logLevel ?? 'warn',
plugins: await createVitePressPlugin( plugins,
config,
ssr,
pageToHashMap,
clientJSMap
),
ssr: { ssr: {
noExternal: ['vitepress', '@docsearch/css'] noExternal: ['vitepress', '@docsearch/css']
}, },
@ -144,12 +137,8 @@ export default async function resolveViteConfig(
ssrEmitAssets: config.mpa, ssrEmitAssets: config.mpa,
// minify with esbuild in MPA mode (for CSS) // minify with esbuild in MPA mode (for CSS)
minify: ssr minify: ssr
? config.mpa ? config.mpa && 'esbuild'
? 'esbuild' : options.minify ?? !process.env.DEBUG,
: false
: typeof options.minify === 'boolean'
? options.minify
: !process.env.DEBUG,
outDir: ssr ? config.tempDir : config.outDir, outDir: ssr ? config.tempDir : config.outDir,
cssCodeSplit: false, cssCodeSplit: false,
rollupOptions: { rollupOptions: {

@ -148,7 +148,7 @@ export async function resolveConfig(
userConfig.concurrency ?? Math.round(cpus().length / 1.5), userConfig.concurrency ?? Math.round(cpus().length / 1.5),
1 // At least one thread required 1 // At least one thread required
), ),
parallel: userConfig.parallel ?? true parallel: userConfig.parallel ?? ['render', 'local-search']
} }
// to be shared with content loaders // to be shared with content loaders

@ -173,9 +173,12 @@ export async function localSearchPlugin(
siteConfig.pages.length, siteConfig.pages.length,
'indexing local search' 'indexing local search'
) )
const parallel = shouldUseParallel(siteConfig, 'local-search')
await pMap( await pMap(
siteConfig.pages, siteConfig.pages,
(page) => indexFile(page, !!siteConfig.parallel).then(updateProgress), (page) => indexFile(page, parallel).then(updateProgress),
{ concurrency } { concurrency }
) )
@ -293,7 +296,7 @@ async function* splitPageIntoSections(
} }
/*=============================== Worker API ===============================*/ /*=============================== Worker API ===============================*/
import { registerWorkload } from '../worker' import { registerWorkload, shouldUseParallel } from '../worker'
import Queue from '../utils/queue' import Queue from '../utils/queue'
// Worker proxy (worker thread) // Worker proxy (worker thread)

@ -1,7 +1,7 @@
import { createServer as createViteServer, type ServerOptions } from 'vite' import { createServer as createViteServer, type ServerOptions } from 'vite'
import { resolveConfig } from './config' import { resolveConfig } from './config'
import { createVitePressPlugin } from './plugin' import { createVitePressPlugin } from './plugin'
import { launchWorkers, stopWorkers } from './worker' import { launchWorkers, stopWorkers, shouldUseParallel } from './worker'
export async function createServer( export async function createServer(
root: string = process.cwd(), root: string = process.cwd(),
@ -10,7 +10,8 @@ export async function createServer(
) { ) {
const config = await resolveConfig(root) const config = await resolveConfig(root)
if (config.parallel) launchWorkers(config.concurrency, { config: config }) if (shouldUseParallel(config))
launchWorkers(config.concurrency, { config: config })
if (serverOptions.base) { if (serverOptions.base) {
config.site.base = serverOptions.base config.site.base = serverOptions.base

@ -13,6 +13,7 @@ import type {
SSGContext, SSGContext,
SiteData SiteData
} from './shared' } from './shared'
import type { SupportsParallel } from './worker'
export type RawConfigExports<ThemeConfig = any> = export type RawConfigExports<ThemeConfig = any> =
| Awaitable<UserConfig<ThemeConfig>> | Awaitable<UserConfig<ThemeConfig>>
@ -165,9 +166,9 @@ export interface UserConfig<ThemeConfig = any>
* 2. Parallel SSR Rendering * 2. Parallel SSR Rendering
* 3. Parallel Local Search Indexing (when using default splitter) * 3. Parallel Local Search Indexing (when using default splitter)
* @experimental * @experimental
* @default true * @default ['render', 'local-search']
*/ */
parallel?: boolean parallel?: boolean | SupportsParallel[]
/** /**
* @experimental * @experimental
@ -263,5 +264,5 @@ export interface SiteConfig<ThemeConfig = any>
logger: Logger logger: Logger
userConfig: UserConfig userConfig: UserConfig
concurrency: number concurrency: number
parallel: boolean parallel: boolean | SupportsParallel[]
} }

@ -1,8 +1,25 @@
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads' import { Worker, isMainThread, parentPort, workerData } from 'worker_threads'
import RpcContext, { deferPromise } from 'rpc-magic-proxy' import RpcContext, { deferPromise } from 'rpc-magic-proxy'
import { task, updateCurrentTask } from './utils/task' import { task, updateCurrentTask } from './utils/task'
import c from 'picocolors'
import Queue from './utils/queue' import Queue from './utils/queue'
import _debug from 'debug' import _debug from 'debug'
import type { SiteConfig } from 'siteConfig'
export type SupportsParallel = 'bundle' | 'render' | 'local-search'
/**
* 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') let debug = _debug('vitepress:worker:main')
const WORKER_MAGIC = 'vitepress:worker' const WORKER_MAGIC = 'vitepress:worker'
@ -66,7 +83,7 @@ export async function launchWorkers(numWorkers: number, context: Object) {
debug: debug.enabled ? debug : null, debug: debug.enabled ? debug : null,
task, task,
updateCurrentTask updateCurrentTask
} as typeof workerMeta, },
initWorkerHooks, initWorkerHooks,
getNextTask, getNextTask,
context context
@ -137,7 +154,7 @@ export function registerWorkload<T extends Object, K extends any[], V>(
// Will keep querying next workload from main thread // Will keep querying next workload from main thread
async function workerMainLoop() { async function workerMainLoop() {
const ctx = new RpcContext(parentPort!) const ctx = new RpcContext({ preserveThis: true }).bind(parentPort!)
const { const {
workerMeta: _workerMeta, workerMeta: _workerMeta,
initWorkerHooks, initWorkerHooks,
@ -174,14 +191,17 @@ async function workerMainLoop() {
try { try {
await init.apply(context) await init.apply(context)
} catch (e) { } catch (e) {
console.error(`worker: failed to init workload "${name}": ${e}`) console.error(c.red(`worker: failed to init workload "${name}":`), e)
} }
el.init = undefined el.init = undefined
} }
try { try {
resolve(await main.apply(context, argv)) resolve(await main.apply(context, argv))
} catch (e) { } catch (e) {
console.error(`worker:${workerMeta.workerId}: task "${name}" error`, e) console.error(
c.red(`worker:${workerMeta.workerId} error running task "${name}":`),
e
)
reject(e) reject(e)
} }
} }

Loading…
Cancel
Save