mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-01 02:12:36 +02:00
perf(core): optimize SSG collected data memory and worker thread communication (#11162)
This commit is contained in:
parent
53fa0ecb1f
commit
33811e38fe
6 changed files with 105 additions and 46 deletions
|
@ -72,12 +72,22 @@ function createPerfLogger(): PerfLoggerAPI {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatMemory = (memory: Memory): string => {
|
const formatBytesToMb = (bytes: number) =>
|
||||||
const fmtHead = (bytes: number) =>
|
logger.cyan(`${(bytes / 1024 / 1024).toFixed(0)}mb`);
|
||||||
logger.cyan(`${(bytes / 1000000).toFixed(0)}mb`);
|
|
||||||
|
const formatMemoryDelta = (memory: Memory): string => {
|
||||||
return logger.dim(
|
return logger.dim(
|
||||||
`(${fmtHead(memory.before.heapUsed)} -> ${fmtHead(
|
`(Heap ${formatBytesToMb(memory.before.heapUsed)} -> ${formatBytesToMb(
|
||||||
memory.after.heapUsed,
|
memory.after.heapUsed,
|
||||||
|
)} / Total ${formatBytesToMb(memory.after.heapTotal)})`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMemoryCurrent = (): string => {
|
||||||
|
const memory = getMemory();
|
||||||
|
return logger.dim(
|
||||||
|
`(Heap ${formatBytesToMb(memory.heapUsed)} / Total ${formatBytesToMb(
|
||||||
|
memory.heapTotal,
|
||||||
)})`,
|
)})`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -103,7 +113,7 @@ function createPerfLogger(): PerfLoggerAPI {
|
||||||
console.log(
|
console.log(
|
||||||
`${PerfPrefix}${formatStatus(error)} ${label} - ${formatDuration(
|
`${PerfPrefix}${formatStatus(error)} ${label} - ${formatDuration(
|
||||||
duration,
|
duration,
|
||||||
)} - ${formatMemory(memory)}`,
|
)} - ${formatMemoryDelta(memory)}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -144,7 +154,9 @@ function createPerfLogger(): PerfLoggerAPI {
|
||||||
};
|
};
|
||||||
|
|
||||||
const log: PerfLoggerAPI['log'] = (label: string) =>
|
const log: PerfLoggerAPI['log'] = (label: string) =>
|
||||||
console.log(`${PerfPrefix} ${applyParentPrefix(label)}`);
|
console.log(
|
||||||
|
`${PerfPrefix} ${applyParentPrefix(label)} - ${formatMemoryCurrent()}`,
|
||||||
|
);
|
||||||
|
|
||||||
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
|
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
|
||||||
const finalLabel = applyParentPrefix(label);
|
const finalLabel = applyParentPrefix(label);
|
||||||
|
|
|
@ -16,8 +16,8 @@ import {
|
||||||
createStatefulBrokenLinks,
|
createStatefulBrokenLinks,
|
||||||
BrokenLinksProvider,
|
BrokenLinksProvider,
|
||||||
} from './BrokenLinksContext';
|
} from './BrokenLinksContext';
|
||||||
import {toPageCollectedMetadata} from './serverHelmetUtils';
|
import {toPageCollectedMetadataInternal} from './serverHelmetUtils';
|
||||||
import type {PageCollectedData, AppRenderer} from '../common';
|
import type {AppRenderer, PageCollectedDataInternal} from '../common';
|
||||||
|
|
||||||
const render: AppRenderer['render'] = async ({
|
const render: AppRenderer['render'] = async ({
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -47,7 +47,7 @@ const render: AppRenderer['render'] = async ({
|
||||||
|
|
||||||
const {helmet} = helmetContext as FilledContext;
|
const {helmet} = helmetContext as FilledContext;
|
||||||
|
|
||||||
const metadata = toPageCollectedMetadata({helmet});
|
const metadata = toPageCollectedMetadataInternal({helmet});
|
||||||
|
|
||||||
// TODO Docusaurus v4 remove with deprecated postBuild({head}) API
|
// TODO Docusaurus v4 remove with deprecated postBuild({head}) API
|
||||||
// the returned collectedData must be serializable to run in workers
|
// the returned collectedData must be serializable to run in workers
|
||||||
|
@ -55,7 +55,7 @@ const render: AppRenderer['render'] = async ({
|
||||||
metadata.helmet = null;
|
metadata.helmet = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectedData: PageCollectedData = {
|
const collectedData: PageCollectedDataInternal = {
|
||||||
metadata,
|
metadata,
|
||||||
anchors: statefulBrokenLinks.getCollectedAnchors(),
|
anchors: statefulBrokenLinks.getCollectedAnchors(),
|
||||||
links: statefulBrokenLinks.getCollectedLinks(),
|
links: statefulBrokenLinks.getCollectedLinks(),
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {ReactElement} from 'react';
|
import type {ReactElement} from 'react';
|
||||||
import type {PageCollectedMetadata} from '../common';
|
import type {PageCollectedMetadataInternal} from '../common';
|
||||||
import type {HelmetServerState} from 'react-helmet-async';
|
import type {HelmetServerState} from 'react-helmet-async';
|
||||||
|
|
||||||
type BuildMetaTag = {name?: string; content?: string};
|
type BuildMetaTag = {name?: string; content?: string};
|
||||||
|
@ -30,11 +30,11 @@ function isNoIndexTag(tag: BuildMetaTag): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toPageCollectedMetadata({
|
export function toPageCollectedMetadataInternal({
|
||||||
helmet,
|
helmet,
|
||||||
}: {
|
}: {
|
||||||
helmet: HelmetServerState;
|
helmet: HelmetServerState;
|
||||||
}): PageCollectedMetadata {
|
}): PageCollectedMetadataInternal {
|
||||||
const tags = getBuildMetaTags(helmet);
|
const tags = getBuildMetaTags(helmet);
|
||||||
const noIndex = tags.some(isNoIndexTag);
|
const noIndex = tags.some(isNoIndexTag);
|
||||||
|
|
||||||
|
|
30
packages/docusaurus/src/common.d.ts
vendored
30
packages/docusaurus/src/common.d.ts
vendored
|
@ -13,7 +13,7 @@ import type {RouteBuildMetadata} from '@docusaurus/types';
|
||||||
|
|
||||||
export type AppRenderResult = {
|
export type AppRenderResult = {
|
||||||
html: string;
|
html: string;
|
||||||
collectedData: PageCollectedData;
|
collectedData: PageCollectedDataInternal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppRenderer = {
|
export type AppRenderer = {
|
||||||
|
@ -40,23 +40,43 @@ export type RouteBuildMetadataInternal = {
|
||||||
script: string;
|
script: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// This data structure must remain serializable!
|
|
||||||
// See why: https://github.com/facebook/docusaurus/pull/10826
|
|
||||||
export type PageCollectedMetadata = {
|
export type PageCollectedMetadata = {
|
||||||
public: RouteBuildMetadata;
|
public: RouteBuildMetadata;
|
||||||
internal: RouteBuildMetadataInternal;
|
|
||||||
// TODO Docusaurus v4 remove legacy unserializable helmet data structure
|
// TODO Docusaurus v4 remove legacy unserializable helmet data structure
|
||||||
// See https://github.com/facebook/docusaurus/pull/10850
|
// See https://github.com/facebook/docusaurus/pull/10850
|
||||||
helmet: HelmetServerState | null;
|
helmet: HelmetServerState | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This data structure must remain serializable!
|
||||||
|
// See why: https://github.com/facebook/docusaurus/pull/10826
|
||||||
|
export type PageCollectedMetadataInternal = PageCollectedMetadata & {
|
||||||
|
internal: {
|
||||||
|
htmlAttributes: string;
|
||||||
|
bodyAttributes: string;
|
||||||
|
title: string;
|
||||||
|
meta: string;
|
||||||
|
link: string;
|
||||||
|
script: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageCollectedDataInternal = {
|
||||||
|
metadata: PageCollectedMetadataInternal;
|
||||||
|
modules: string[];
|
||||||
|
links: string[];
|
||||||
|
anchors: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep this data structure as small as possible
|
||||||
|
// See https://github.com/facebook/docusaurus/pull/11162
|
||||||
export type PageCollectedData = {
|
export type PageCollectedData = {
|
||||||
metadata: PageCollectedMetadata;
|
metadata: PageCollectedMetadata;
|
||||||
links: string[];
|
links: string[];
|
||||||
anchors: string[];
|
anchors: string[];
|
||||||
modules: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keep this data structure as small as possible
|
||||||
|
// See https://github.com/facebook/docusaurus/pull/11162
|
||||||
export type SiteCollectedData = {
|
export type SiteCollectedData = {
|
||||||
[pathname: string]: PageCollectedData;
|
[pathname: string]: PageCollectedData;
|
||||||
};
|
};
|
||||||
|
|
|
@ -38,16 +38,13 @@ const createSimpleSSGExecutor: CreateSSGExecutor = async ({
|
||||||
}) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
run: () => {
|
run: () => {
|
||||||
return PerfLogger.async(
|
return PerfLogger.async('SSG (current thread)', async () => {
|
||||||
'Generate static files (current thread)',
|
|
||||||
async () => {
|
|
||||||
const ssgResults = await executeSSGInlineTask({
|
const ssgResults = await executeSSGInlineTask({
|
||||||
pathnames,
|
pathnames,
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
return createGlobalSSGResult(ssgResults);
|
return createGlobalSSGResult(ssgResults);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
destroy: async () => {
|
destroy: async () => {
|
||||||
|
@ -111,7 +108,7 @@ const createPooledSSGExecutor: CreateSSGExecutor = async ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const pool = await PerfLogger.async(
|
const pool = await PerfLogger.async(
|
||||||
`Create SSG pool - ${logger.cyan(numberOfThreads)} threads`,
|
`Create SSG thread pool - ${logger.cyan(numberOfThreads)} threads`,
|
||||||
async () => {
|
async () => {
|
||||||
const Tinypool = await import('tinypool').then((m) => m.default);
|
const Tinypool = await import('tinypool').then((m) => m.default);
|
||||||
|
|
||||||
|
@ -134,13 +131,17 @@ const createPooledSSGExecutor: CreateSSGExecutor = async ({
|
||||||
const pathnamesChunks = _.chunk(pathnames, SSGWorkerThreadTaskSize);
|
const pathnamesChunks = _.chunk(pathnames, SSGWorkerThreadTaskSize);
|
||||||
|
|
||||||
// Tiny wrapper for type-safety
|
// Tiny wrapper for type-safety
|
||||||
const submitTask: ExecuteSSGWorkerThreadTask = (task) => pool.run(task);
|
const submitTask: ExecuteSSGWorkerThreadTask = async (task) => {
|
||||||
|
const result = await pool.run(task);
|
||||||
|
// Note, we don't use PerfLogger.async() because all tasks are submitted
|
||||||
|
// immediately at once and queued, while results are received progressively
|
||||||
|
PerfLogger.log(`Result for task ${logger.name(task.id)}`);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
run: async () => {
|
run: async () => {
|
||||||
const results = await PerfLogger.async(
|
const results = await PerfLogger.async(`Thread pool`, async () => {
|
||||||
`Generate static files (${numberOfThreads} worker threads)`,
|
|
||||||
async () => {
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
pathnamesChunks.map((taskPathnames, taskIndex) => {
|
pathnamesChunks.map((taskPathnames, taskIndex) => {
|
||||||
return submitTask({
|
return submitTask({
|
||||||
|
@ -149,8 +150,7 @@ const createPooledSSGExecutor: CreateSSGExecutor = async ({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
const allResults = results.flat();
|
const allResults = results.flat();
|
||||||
return createGlobalSSGResult(allResults);
|
return createGlobalSSGResult(allResults);
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,14 +22,18 @@ import {SSGConcurrency} from './ssgEnv';
|
||||||
import {writeStaticFile} from './ssgUtils';
|
import {writeStaticFile} from './ssgUtils';
|
||||||
import {createSSGRequire} from './ssgNodeRequire';
|
import {createSSGRequire} from './ssgNodeRequire';
|
||||||
import type {SSGParams} from './ssgParams';
|
import type {SSGParams} from './ssgParams';
|
||||||
import type {AppRenderer, AppRenderResult} from '../common';
|
import type {
|
||||||
|
AppRenderer,
|
||||||
|
PageCollectedData,
|
||||||
|
PageCollectedDataInternal,
|
||||||
|
} from '../common';
|
||||||
import type {HtmlMinifier} from '@docusaurus/bundler';
|
import type {HtmlMinifier} from '@docusaurus/bundler';
|
||||||
|
|
||||||
export type SSGSuccess = {
|
export type SSGSuccess = {
|
||||||
success: true;
|
success: true;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
result: {
|
result: {
|
||||||
collectedData: AppRenderResult['collectedData'];
|
collectedData: PageCollectedData;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
// html: we don't include it on purpose!
|
// html: we don't include it on purpose!
|
||||||
// we don't need to aggregate all html contents in memory!
|
// we don't need to aggregate all html contents in memory!
|
||||||
|
@ -144,6 +148,26 @@ export async function loadSSGRenderer({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We reduce the page collected data structure after the HTML file is written
|
||||||
|
// Some data (modules, metadata.internal) is only useful to create the HTML file
|
||||||
|
// It's not useful to aggregate that collected data in memory
|
||||||
|
// Keep this data structure as small as possible
|
||||||
|
// See https://github.com/facebook/docusaurus/pull/11162
|
||||||
|
function reduceCollectedData(
|
||||||
|
pageCollectedData: PageCollectedDataInternal,
|
||||||
|
): PageCollectedData {
|
||||||
|
// We re-create the object from scratch
|
||||||
|
// We absolutely want to avoid TS duck typing
|
||||||
|
return {
|
||||||
|
anchors: pageCollectedData.anchors,
|
||||||
|
metadata: {
|
||||||
|
public: pageCollectedData.metadata.public,
|
||||||
|
helmet: pageCollectedData.metadata.helmet,
|
||||||
|
},
|
||||||
|
links: pageCollectedData.links,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function generateStaticFile({
|
async function generateStaticFile({
|
||||||
pathname,
|
pathname,
|
||||||
appRenderer,
|
appRenderer,
|
||||||
|
@ -176,11 +200,14 @@ async function generateStaticFile({
|
||||||
content: minifierResult.code,
|
content: minifierResult.code,
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const collectedData = reduceCollectedData(appRenderResult.collectedData);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
pathname,
|
pathname,
|
||||||
result: {
|
result: {
|
||||||
collectedData: appRenderResult.collectedData,
|
collectedData,
|
||||||
// As of today, only the html minifier can emit SSG warnings
|
// As of today, only the html minifier can emit SSG warnings
|
||||||
warnings: minifierResult.warnings,
|
warnings: minifierResult.warnings,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue