diff --git a/packages/docs-infra/src/CodeHighlighter/loadFallbackCode.ts b/packages/docs-infra/src/CodeHighlighter/loadFallbackCode.ts index f6f99d02f..8ccc2e0ed 100644 --- a/packages/docs-infra/src/CodeHighlighter/loadFallbackCode.ts +++ b/packages/docs-infra/src/CodeHighlighter/loadFallbackCode.ts @@ -8,6 +8,7 @@ import type { } from './types'; import { loadVariant } from './loadVariant'; import { getFileNameFromUrl } from '../pipeline/loaderUtils'; +import { nameMark } from '../pipeline/loadPrecomputedCodeHighlighter/performanceLogger'; // Helper function to get the source for a specific filename from a variant async function getFileSource( @@ -82,6 +83,11 @@ export async function loadFallbackCode( } = options; loaded = { ...loaded }; + const functionName = 'Load Fallback Code'; + const startMark = nameMark(functionName, 'Start Loading', [url]); + performance.mark(startMark); + let currentMark = startMark; + // Step 1: Ensure we have the initial variant loaded let initial = loaded[initialVariant]; if (!initial) { @@ -99,6 +105,15 @@ export async function loadFallbackCode( if (!initial) { throw new Error(`Initial variant "${initialVariant}" not found in loaded code.`); } + + const loadedCodeMetaMark = nameMark(functionName, 'Loaded Code Meta', [url]); + performance.mark(loadedCodeMetaMark); + performance.measure( + nameMark(functionName, 'Code Meta Loading', [url]), + currentMark, + loadedCodeMetaMark, + ); + currentMark = loadedCodeMetaMark; } // Check if we can return early after loadCodeMeta @@ -131,6 +146,15 @@ export async function loadFallbackCode( ); } + const loadedMainFileMark = nameMark(functionName, 'Loaded Main File', [url]); + performance.mark(loadedMainFileMark); + performance.measure( + nameMark(functionName, 'Main File Loading', [url]), + currentMark, + loadedMainFileMark, + ); + currentMark = loadedMainFileMark; + // If we need highlighting and have a string source, parse it if (shouldHighlight && typeof fileSource === 'string' && sourceParser && actualFilename) { try { @@ -141,6 +165,15 @@ export async function loadFallbackCode( `Failed to parse source for highlighting (variant: ${initialVariant}, file: ${actualFilename}): ${JSON.stringify(error)}`, ); } + + const parsedMainFileMark = nameMark(functionName, 'Parsed Main File', [url]); + performance.mark(parsedMainFileMark); + performance.measure( + nameMark(functionName, 'Main File Parsing', [url]), + currentMark, + parsedMainFileMark, + ); + currentMark = parsedMainFileMark; } else if (shouldHighlight && typeof fileSource === 'string' && !actualFilename) { // Create basic HAST node when we can't parse due to missing filename // This marks that the source has passed through the parsing pipeline @@ -183,6 +216,17 @@ export async function loadFallbackCode( if (loadVariantMeta) { // Use provided loadVariantMeta function quickVariant = await loadVariantMeta(initialVariant, initial); + + const loadedInitialVariantMetaMark = nameMark(functionName, 'Loaded Initial Variant Meta', [ + url, + ]); + performance.mark(loadedInitialVariantMetaMark); + performance.measure( + nameMark(functionName, 'Initial Variant Meta Loading', [url]), + currentMark, + loadedInitialVariantMetaMark, + ); + currentMark = loadedInitialVariantMetaMark; } else { // Create a basic variant using fallback logic quickVariant = { @@ -191,6 +235,8 @@ export async function loadFallbackCode( }; } + const beforeInitialVariantMark = currentMark; + loaded = { ...loaded, [initialVariant]: quickVariant }; initial = quickVariant; @@ -213,6 +259,18 @@ export async function loadFallbackCode( const result = await getFileSource(quickVariant, initialFilename, loadSource); fileSource = result.source; actualFilename = result.filename; + + const loadedInitialFileMark = nameMark(functionName, 'Loaded Initial File', [ + initialFilename || 'unknown', + url, + ]); + performance.mark(loadedInitialFileMark); + performance.measure( + nameMark(functionName, 'Initial File Loading', [initialFilename || 'unknown', url]), + currentMark, + loadedInitialFileMark, + ); + currentMark = loadedInitialFileMark; } catch (error) { throw new Error( `Failed to get source for file ${initialFilename || quickVariant.fileName} in variant ${initialVariant}: ${error}`, @@ -224,6 +282,18 @@ export async function loadFallbackCode( try { const parseSource = await sourceParser; fileSource = parseSource(fileSource, actualFilename); + + const parsedInitialFileMark = nameMark(functionName, 'Parsed Initial File', [ + initialFilename || 'unknown', + url, + ]); + performance.mark(parsedInitialFileMark); + performance.measure( + nameMark(functionName, 'Initial File Parsing', [initialFilename || 'unknown', url]), + currentMark, + parsedInitialFileMark, + ); + currentMark = parsedInitialFileMark; } catch (error) { throw new Error( `Failed to parse source for highlighting (variant: ${initialVariant}, file: ${actualFilename}): ${JSON.stringify(error)}`, @@ -253,6 +323,15 @@ export async function loadFallbackCode( loaded = { ...loaded, [initialVariant]: initial }; } + const loadedInitialFilesMark = nameMark(functionName, 'Loaded Initial Files', [url], true); + performance.mark(loadedInitialFilesMark); + performance.measure( + nameMark(functionName, 'Initial Files Loading', [url], true), + beforeInitialVariantMark, + loadedInitialFilesMark, + ); + currentMark = loadedInitialFilesMark; + // Early return - we have all the info we need return { code: loaded, @@ -269,6 +348,8 @@ export async function loadFallbackCode( } } + const beforeGlobalsMark = currentMark; + // Step 2b: Fall back to full loadVariant processing // Load globalsCode - convert string URLs to Code objects, keep Code objects as-is let globalsCodeObjects: Array | undefined; @@ -283,7 +364,21 @@ export async function loadFallbackCode( if (typeof globalItem === 'string') { // String URL - load Code object via loadCodeMeta try { - return await loadCodeMeta!(globalItem); + const codeMeta = await loadCodeMeta!(globalItem); + + const loadedGlobalCodeMark = nameMark(functionName, 'Loaded Global Code Meta', [ + globalItem, + url, + ]); + performance.mark(loadedGlobalCodeMark); + performance.measure( + nameMark(functionName, 'Global Code Meta Loading', [globalItem, url]), + currentMark, + loadedGlobalCodeMark, + ); + currentMark = loadedGlobalCodeMark; + + return codeMeta; } catch (error) { throw new Error( `Failed to load globalsCode from URL: ${globalItem}. Error: ${JSON.stringify(error)}`, @@ -296,6 +391,15 @@ export async function loadFallbackCode( }); globalsCodeObjects = await Promise.all(globalsPromises); + + const loadedGlobalCodeMark = nameMark(functionName, 'Loaded Globals Meta', [url], true); + performance.mark(loadedGlobalCodeMark); + performance.measure( + nameMark(functionName, 'Globals Meta Loading', [url], true), + beforeGlobalsMark, + loadedGlobalCodeMark, + ); + currentMark = loadedGlobalCodeMark; } // Convert globalsCodeObjects to VariantCode | string for this specific variant @@ -323,6 +427,15 @@ export async function loadFallbackCode( output, }); + const loadedInitialVariantMark = nameMark(functionName, 'Loaded Initial Variant', [url], true); + performance.mark(loadedInitialVariantMark); + performance.measure( + nameMark(functionName, 'Initial Variant Loading', [url], true), + currentMark, + loadedInitialVariantMark, + ); + currentMark = loadedInitialVariantMark; + // Update the loaded code with the processed variant loaded = { ...loaded, [initialVariant]: loadedVariant }; initial = loadedVariant; @@ -345,6 +458,8 @@ export async function loadFallbackCode( // Step 4: Handle fallbackUsesAllVariants - load all variants to get all possible files if (fallbackUsesAllVariants) { + const beforeAllVariantMark = currentMark; + // Determine all variants to process - use provided variants or infer from loaded code const allVariants = variants || Object.keys(loaded || {}); @@ -367,6 +482,17 @@ export async function loadFallbackCode( variant = allCode[variantName]; // Update loaded with all variants from loadCodeMeta loaded = { ...loaded, ...allCode }; + + const loadedInitialCodeMetaMark = nameMark(functionName, 'Loaded Initial Code Meta', [ + url, + ]); + performance.mark(loadedInitialCodeMetaMark); + performance.measure( + nameMark(functionName, 'Initial Code Meta Loading', [url]), + currentMark, + loadedInitialCodeMetaMark, + ); + currentMark = loadedInitialCodeMetaMark; } catch (error) { console.warn(`Failed to load code meta for variant ${variantName}: ${error}`); return { variantName, loadedVariant: null, fileNames: [] }; @@ -410,6 +536,20 @@ export async function loadFallbackCode( fileNames.push(...Object.keys(loadedVariant.extraFiles)); } + const loadedInitialVariantMark = nameMark( + functionName, + 'Loaded Initial Variant', + [variantName, url], + true, + ); + performance.mark(loadedInitialVariantMark); + performance.measure( + nameMark(functionName, 'Initial Variant Loading', [variantName, url], true), + currentMark, + loadedInitialVariantMark, + ); + currentMark = loadedInitialVariantMark; + return { variantName, loadedVariant, fileNames }; } catch (error) { // Log but don't fail - we want to get as many file names as possible @@ -428,6 +568,20 @@ export async function loadFallbackCode( fileNames.forEach((fileName) => allFileNames.add(fileName)); }); } + + const loadedInitialVariantsMark = nameMark( + functionName, + 'Loaded Initial Variants', + [url], + true, + ); + performance.mark(loadedInitialVariantsMark); + performance.measure( + nameMark(functionName, 'Initial Variants Loading', [url], true), + beforeAllVariantMark, + loadedInitialVariantsMark, + ); + currentMark = loadedInitialVariantsMark; } // Ensure we have the latest initial variant data @@ -458,6 +612,14 @@ export async function loadFallbackCode( finalFilename = finalInitial.fileName; } + const loadedInitialFileMark = nameMark(functionName, 'Loaded Initial File', [url]); + performance.mark(loadedInitialFileMark); + performance.measure( + nameMark(functionName, 'Initial File Loading', [url]), + currentMark, + loadedInitialFileMark, + ); + return { code: loaded, initialFilename: finalFilename, diff --git a/packages/docs-infra/src/CodeHighlighter/loadVariant.ts b/packages/docs-infra/src/CodeHighlighter/loadVariant.ts index 6dde408f6..50974ba3c 100644 --- a/packages/docs-infra/src/CodeHighlighter/loadVariant.ts +++ b/packages/docs-infra/src/CodeHighlighter/loadVariant.ts @@ -17,6 +17,7 @@ import type { LoadVariantOptions, Externals, } from './types'; +import { nameMark } from '../pipeline/loadPrecomputedCodeHighlighter/performanceLogger'; function compressAsync(input: Uint8Array, options: AsyncGzipOptions = {}): Promise { return new Promise((resolve, reject) => { @@ -163,6 +164,11 @@ async function loadSingleFile( let extraDependenciesFromSource: string[] | undefined; let externalsFromSource: Externals | undefined; + const functionName = 'Load Variant File'; + const startMark = nameMark(functionName, 'Start Loading', [url || fileName]); + performance.mark(startMark); + let currentMark = startMark; + // Load source if not provided if (!finalSource) { if (!loadSource) { @@ -187,6 +193,15 @@ async function loadSingleFile( extraDependenciesFromSource = loadResult.extraDependencies; externalsFromSource = loadResult.externals; + const loadedFileMark = nameMark(functionName, 'Loaded File', [url]); + performance.mark(loadedFileMark); + performance.measure( + nameMark(functionName, 'File Loading', [url]), + currentMark, + loadedFileMark, + ); + currentMark = loadedFileMark; + // Validate that extraFiles from loadSource contain only absolute URLs as values if (extraFilesFromSource) { for (const [extraFileName, fileData] of Object.entries(extraFilesFromSource)) { @@ -278,6 +293,15 @@ async function loadSingleFile( normalizePathKey(fileName), sourceTransformers, ); + + const transformedFileMark = nameMark(functionName, 'Transformed File', [url || fileName]); + performance.mark(transformedFileMark); + performance.measure( + nameMark(functionName, 'File Transforming', [url || fileName]), + currentMark, + transformedFileMark, + ); + currentMark = transformedFileMark; } // Parse source if it's a string and parsing is not disabled @@ -293,6 +317,15 @@ async function loadSingleFile( const parseSource = await sourceParser; finalSource = parseSource(finalSource, fileName); + const parsedFileMark = nameMark(functionName, 'Parsed File', [url || fileName]); + performance.mark(parsedFileMark); + performance.measure( + nameMark(functionName, 'File Parsing', [url || fileName]), + currentMark, + parsedFileMark, + ); + currentMark = parsedFileMark; + if (finalTransforms && !disableTransforms) { finalTransforms = await transformParsedSource( sourceString, @@ -301,6 +334,17 @@ async function loadSingleFile( finalTransforms, parseSource, ); + + const transformParsedFileMark = nameMark(functionName, 'Transform Parsed File', [ + url || fileName, + ]); + performance.mark(transformParsedFileMark); + performance.measure( + nameMark(functionName, 'Parsed File Transforming', [url || fileName]), + currentMark, + transformParsedFileMark, + ); + currentMark = transformParsedFileMark; } if (options.output === 'hastGzip' && process.env.NODE_ENV === 'production') { @@ -308,9 +352,29 @@ async function loadSingleFile( await compressAsync(strToU8(JSON.stringify(finalSource)), { consume: true, level: 9 }), ); finalSource = { hastGzip }; + + const compressedFileMark = nameMark(functionName, 'Compressed File', [url || fileName]); + performance.mark(compressedFileMark); + performance.measure( + nameMark(functionName, 'File Compression', [url || fileName]), + currentMark, + compressedFileMark, + ); + currentMark = compressedFileMark; } else if (options.output === 'hastJson' || options.output === 'hastGzip') { // in development, we skip compression but still convert to JSON finalSource = { hastJson: JSON.stringify(finalSource) }; + + const compressedFileMark = nameMark(functionName, 'JSON Stringified File', [ + url || fileName, + ]); + performance.mark(compressedFileMark); + performance.measure( + nameMark(functionName, 'File Stringification', [url || fileName]), + currentMark, + compressedFileMark, + ); + currentMark = compressedFileMark; } } catch (error) { throw new Error( @@ -557,6 +621,11 @@ export async function loadVariant( }> >(); + const functionName = 'Load Variant'; + const startMark = nameMark(functionName, 'Start Loading', [url || variantName]); + performance.mark(startMark); + let currentMark = startMark; + if (typeof variant === 'string') { if (!loadVariantMeta) { // Create a basic loadVariantMeta function as fallback @@ -579,6 +648,17 @@ export async function loadVariant( `Failed to load variant code (variant: ${variantName}, url: ${variant}): ${JSON.stringify(error)}`, ); } + + const loadedVariantMetaMark = nameMark(functionName, 'Loaded Variant Meta', [ + url || variantName, + ]); + performance.mark(loadedVariantMetaMark); + performance.measure( + nameMark(functionName, 'Variant Meta Loading', [url || variantName]), + currentMark, + loadedVariantMetaMark, + ); + currentMark = loadedVariantMetaMark; } } @@ -653,12 +733,14 @@ export async function loadVariant( allFilesUsed.push(...mainFileResult.extraDependencies); } - // Add externals from main file loading - if (mainFileResult.externals) { - allExternals = mergeExternals([allExternals, mainFileResult.externals]); - } - - let allExtraFiles: VariantExtraFiles = {}; + const loadedMainFileMark = nameMark(functionName, 'Loaded Main File', [url || fileName], true); + performance.mark(loadedMainFileMark); + performance.measure( + nameMark(functionName, 'Main File Loading', [url || fileName], true), + currentMark, + loadedMainFileMark, + ); + currentMark = loadedMainFileMark; // Validate extraFiles keys from variant definition if (variant.extraFiles) { @@ -679,6 +761,20 @@ export async function loadVariant( ...(mainFileResult.extraFiles || {}), }; + // Add externals from main file loading + if (mainFileResult.externals) { + allExternals = mergeExternals([allExternals, mainFileResult.externals]); + } + + const externalsMergedMark = nameMark(functionName, 'Externals Merged', [url || fileName]); + performance.mark(externalsMergedMark); + performance.measure( + nameMark(functionName, 'Merging Externals', [url || fileName]), + currentMark, + externalsMergedMark, + ); + currentMark = externalsMergedMark; + // Track which files come from globals for metadata marking const globalsFileKeys = new Set(); // Track globals file keys for loadExtraFiles @@ -719,6 +815,22 @@ export async function loadVariant( } else { try { globalsVariant = await loadVariantMeta(variantName, globalsItem); + + const globalsVariantMetaLoadedMark = nameMark( + functionName, + 'Globals Variant Meta Loaded', + [globalsItem, url || fileName], + ); + performance.mark(globalsVariantMetaLoadedMark); + performance.measure( + nameMark(functionName, 'Globals Variant Meta Loading', [ + globalsItem, + url || fileName, + ]), + currentMark, + globalsVariantMetaLoadedMark, + ); + currentMark = globalsVariantMetaLoadedMark; } catch (error) { throw new Error( `Failed to load globalsCode variant metadata (variant: ${variantName}, url: ${globalsItem}): ${JSON.stringify(error)}`, @@ -737,6 +849,22 @@ export async function loadVariant( globalsVariant, { ...options, globalsCode: undefined }, // Prevent infinite recursion ); + + const globalsVariantLoadedMark = nameMark(functionName, 'Globals Variant Loaded', [ + globalsVariant.url || variantName, + url || fileName, + ]); + performance.mark(globalsVariantLoadedMark); + performance.measure( + nameMark(functionName, 'Globals Variant Loading', [ + globalsVariant.url || variantName, + url || fileName, + ]), + currentMark, + globalsVariantLoadedMark, + ); + currentMark = globalsVariantLoadedMark; + return globalsResult; } catch (error) { throw new Error( @@ -780,6 +908,17 @@ export async function loadVariant( } } + const globalsLoadedMark = nameMark(functionName, 'Globals Loaded', [url || fileName], true); + performance.mark(globalsLoadedMark); + performance.measure( + nameMark(functionName, 'Globals Loading', [url || fileName], true), + externalsMergedMark, + globalsLoadedMark, + ); + currentMark = globalsLoadedMark; + + let allExtraFiles: VariantExtraFiles = {}; + // Load all extra files if any exist and we have a URL if (Object.keys(extraFilesToLoad).length > 0) { if (!url) { @@ -861,6 +1000,20 @@ export async function loadVariant( allFilesUsed.push(...extraFilesResult.allFilesUsed); allExternals = mergeExternals([allExternals, extraFilesResult.allExternals]); } + + const extraFilesLoadedMark = nameMark( + functionName, + 'Extra Files Loaded', + [url || fileName], + true, + ); + performance.mark(extraFilesLoadedMark); + performance.measure( + nameMark(functionName, 'Extra Files Loading', [url || fileName], true), + currentMark, + extraFilesLoadedMark, + ); + currentMark = extraFilesLoadedMark; } // Note: metadata marking is now handled during loadExtraFiles processing diff --git a/packages/docs-infra/src/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.ts b/packages/docs-infra/src/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.ts index bfe8107a6..030fa7e09 100644 --- a/packages/docs-infra/src/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.ts +++ b/packages/docs-infra/src/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.ts @@ -1,3 +1,7 @@ +// webpack does not like node: imports +// eslint-disable-next-line n/prefer-node-protocol +import path from 'path'; + import type { LoaderContext } from 'webpack'; import { loadVariant } from '../../CodeHighlighter/loadVariant'; import { createParseSource } from '../parseSource'; @@ -9,11 +13,19 @@ import { resolveVariantPathsWithFs } from '../loaderUtils/resolveModulePathWithF import { replacePrecomputeValue } from './replacePrecomputeValue'; import { createLoadServerSource } from '../loadServerSource'; import { getFileNameFromUrl } from '../loaderUtils'; +import { createPerformanceLogger, logPerformance, nameMark } from './performanceLogger'; export type LoaderOptions = { + performance?: { + logging?: boolean; + notableMs?: number; + showWrapperMeasures?: boolean; + }; output?: 'hast' | 'hastJson' | 'hastGzip'; }; +const functionName = 'Load Precomputed Code Highlighter'; + /** * Webpack loader that processes demo files and precomputes variant data. * @@ -32,10 +44,36 @@ export async function loadPrecomputedCodeHighlighter( const callback = this.async(); this.cacheable(); + const options = this.getOptions(); + const performanceNotableMs = options.performance?.notableMs ?? 100; + const performanceShowWrapperMeasures = options.performance?.showWrapperMeasures ?? false; + + let observer: PerformanceObserver | undefined = undefined; + if (options.performance?.logging) { + observer = new PerformanceObserver( + createPerformanceLogger(performanceNotableMs, performanceShowWrapperMeasures), + ); + observer.observe({ entryTypes: ['measure'] }); + } + + const relativePath = path.relative(this.rootContext || process.cwd(), this.resourcePath); + const startMark = nameMark(functionName, 'Start Loading', [relativePath]); + performance.mark(startMark); + let currentMark = startMark; + try { // Parse the source to find a single createDemo call const demoCall = await parseCreateFactoryCall(source, this.resourcePath); + const parsedFactoryMark = nameMark(functionName, 'Parsed Factory', [relativePath]); + performance.mark(parsedFactoryMark); + performance.measure( + nameMark(functionName, 'Factory Parsing', [relativePath]), + currentMark, + parsedFactoryMark, + ); + currentMark = parsedFactoryMark; + // If no createDemo call found, return the source unchanged if (!demoCall) { callback(null, source); @@ -55,6 +93,15 @@ export async function loadPrecomputedCodeHighlighter( // Resolve all variant entry point paths using resolveVariantPathsWithFs const resolvedVariantMap = await resolveVariantPathsWithFs(demoCall.variants || {}); + const pathsResolvedMark = nameMark(functionName, 'Paths Resolved', [relativePath]); + performance.mark(pathsResolvedMark); + performance.measure( + nameMark(functionName, 'Path Resolution', [relativePath]), + currentMark, + pathsResolvedMark, + ); + currentMark = pathsResolvedMark; + // Create loader functions const loadSource = createLoadServerSource({ includeDependencies: true, @@ -69,6 +116,15 @@ export async function loadPrecomputedCodeHighlighter( // Create sourceParser promise for syntax highlighting const sourceParser = createParseSource(); + const functionsInitMark = nameMark(functionName, 'Functions Init', [relativePath]); + performance.mark(functionsInitMark); + performance.measure( + nameMark(functionName, 'Functions Init', [relativePath]), + currentMark, + functionsInitMark, + ); + currentMark = functionsInitMark; + // Process variants in parallel const variantPromises = Array.from(resolvedVariantMap.entries()).map( async ([variantName, fileUrl]) => { @@ -103,6 +159,20 @@ export async function loadPrecomputedCodeHighlighter( }, ); + const variantLoadedMark = nameMark( + functionName, + 'Variant Loaded', + [variantName, relativePath], + true, + ); + performance.mark(variantLoadedMark); + performance.measure( + nameMark(functionName, 'Variant Loading', [variantName, relativePath], true), + currentMark, + variantLoadedMark, + ); + currentMark = variantLoadedMark; + return { variantName, variantData: processedVariant, // processedVariant is a complete VariantCode @@ -126,17 +196,49 @@ export async function loadPrecomputedCodeHighlighter( } } + const variantsLoadedMark = nameMark(functionName, 'All Variants Loaded', [relativePath], true); + performance.mark(variantsLoadedMark); + performance.measure( + nameMark(functionName, 'Complete Variants Loading', [relativePath], true), + functionsInitMark, + variantsLoadedMark, + ); + currentMark = variantsLoadedMark; + // Replace the factory function call with the actual precomputed data const modifiedSource = replacePrecomputeValue(source, variantData, demoCall); + const replacedPrecomputeMark = nameMark(functionName, 'Replaced Precompute', [relativePath]); + performance.mark(replacedPrecomputeMark); + performance.measure( + nameMark(functionName, 'Precompute Replacement', [relativePath]), + currentMark, + replacedPrecomputeMark, + ); + currentMark = replacedPrecomputeMark; + // Add all dependencies to webpack's watch list allDependencies.forEach((dep) => { // Strip 'file://' prefix if present before adding to webpack's dependency tracking this.addDependency(dep.startsWith('file://') ? dep.slice(7) : dep); }); + // log any pending performance entries before completing + observer + ?.takeRecords() + ?.forEach((entry) => + logPerformance(entry, performanceNotableMs, performanceShowWrapperMeasures), + ); + observer?.disconnect(); callback(null, modifiedSource); } catch (error) { + // log any pending performance entries before completing + observer + ?.takeRecords() + ?.forEach((entry) => + logPerformance(entry, performanceNotableMs, performanceShowWrapperMeasures), + ); + observer?.disconnect(); callback(error instanceof Error ? error : new Error(String(error))); } } diff --git a/packages/docs-infra/src/pipeline/loadPrecomputedCodeHighlighter/performanceLogger.ts b/packages/docs-infra/src/pipeline/loadPrecomputedCodeHighlighter/performanceLogger.ts new file mode 100644 index 000000000..db479f099 --- /dev/null +++ b/packages/docs-infra/src/pipeline/loadPrecomputedCodeHighlighter/performanceLogger.ts @@ -0,0 +1,37 @@ +export function logPerformance( + entry: PerformanceEntry, + notableMs: number, + showWrapperMeasures: boolean, +) { + if (entry.duration < notableMs) { + return; + } + + let delim = '-'; + let message = entry.name; + if (message.startsWith('| ')) { + if (!showWrapperMeasures) { + return; + } + + delim = '|'; + message = message.slice(2); + } + + const duration = Math.round(entry.duration).toString().padStart(4, ' '); + console.warn(`${duration}ms ${delim} ${message}`); +} + +export function createPerformanceLogger(notableMs: number, showWrapperMeasures: boolean) { + const performanceLogger: PerformanceObserverCallback = (list) => { + for (const entry of list.getEntries()) { + logPerformance(entry, notableMs, showWrapperMeasures); + } + }; + + return performanceLogger; +} + +export function nameMark(functionName: string, event: string, context: string[], wrapper = false) { + return `${wrapper ? '| ' : ''}${functionName} ${wrapper ? '|' : '-'} ${event} - ${context.join(' - ')}`; +} diff --git a/packages/docs-infra/src/withDocsInfra/withDocsInfra.test.ts b/packages/docs-infra/src/withDocsInfra/withDocsInfra.test.ts index ee80347b2..488c4edf8 100644 --- a/packages/docs-infra/src/withDocsInfra/withDocsInfra.test.ts +++ b/packages/docs-infra/src/withDocsInfra/withDocsInfra.test.ts @@ -64,12 +64,17 @@ describe('withDocsInfra', () => { loaders: [ { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output: 'hastGzip' }, + options: { performance: {}, output: 'hastGzip' }, }, ], }, './app/**/demos/*/client.ts': { - loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'], + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: {} }, + }, + ], }, }); }); @@ -88,23 +93,33 @@ describe('withDocsInfra', () => { loaders: [ { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output: 'hastGzip' }, + options: { performance: {}, output: 'hastGzip' }, }, ], }, './app/**/demos/*/client.ts': { - loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'], + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: {} }, + }, + ], }, './app/**/demos/*/demo-*/index.ts': { loaders: [ { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output: 'hastGzip' }, + options: { performance: {}, output: 'hastGzip' }, }, ], }, './app/**/demos/*/demo-*/client.ts': { - loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'], + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: {} }, + }, + ], }, }); }); @@ -124,12 +139,17 @@ describe('withDocsInfra', () => { loaders: [ { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output: 'hastGzip' }, + options: { performance: {}, output: 'hastGzip' }, }, ], }, './app/**/demos/*/client.ts': { - loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'], + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: {} }, + }, + ], }, './custom/**/*.ts': { loaders: ['custom-loader'], @@ -158,12 +178,17 @@ describe('withDocsInfra', () => { loaders: [ { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output: 'hastGzip' }, + options: { performance: {}, output: 'hastGzip' }, }, ], }, './app/**/demos/*/client.ts': { - loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'], + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: {} }, + }, + ], }, }); }); @@ -204,7 +229,7 @@ describe('withDocsInfra', () => { mockDefaultLoaders.babel, { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output: 'hastGzip' }, + options: { performance: {}, output: 'hastGzip' }, }, ], }); @@ -212,7 +237,10 @@ describe('withDocsInfra', () => { test: new RegExp('/demos/[^/]+/client\\.ts$'), use: [ mockDefaultLoaders.babel, - '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: {} }, + }, ], }); }); @@ -382,23 +410,33 @@ describe('withDocsInfra', () => { loaders: [ { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output: 'hastGzip' }, + options: { performance: {}, output: 'hastGzip' }, }, ], }, './app/**/demos/*/client.ts': { - loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'], + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: {} }, + }, + ], }, './app/**/demos/*/demo-*/index.ts': { loaders: [ { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output: 'hastGzip' }, + options: { performance: {}, output: 'hastGzip' }, }, ], }, './app/**/demos/*/demo-*/client.ts': { - loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'], + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: {} }, + }, + ], }, }); @@ -441,6 +479,183 @@ describe('withDocsInfra', () => { expect(originalDemoClientRule).toBeDefined(); }); }); + + describe('performance options', () => { + it('should pass performance options to turbopack loaders', () => { + const performanceOptions = { + logging: true, + notableMs: 500, + showWrapperMeasures: true, + }; + + const plugin = withDocsInfra({ performance: performanceOptions }); + const result = plugin({}); + + expect(result.turbopack?.rules).toEqual({ + './app/**/demos/*/index.ts': { + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', + options: { performance: performanceOptions, output: 'hastGzip' }, + }, + ], + }, + './app/**/demos/*/client.ts': { + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: performanceOptions }, + }, + ], + }, + }); + }); + + it('should pass performance options to webpack loaders', () => { + const performanceOptions = { + logging: true, + notableMs: 1000, + showWrapperMeasures: false, + }; + + const plugin = withDocsInfra({ performance: performanceOptions }); + const result = plugin({}); + + const mockWebpackConfig: WebpackConfig = { + module: { + rules: [], + }, + }; + + const mockWebpackOptions = { + buildId: 'test-build', + dev: false, + isServer: false, + config: {}, + defaultLoaders: { + babel: { + test: /\.(js|jsx|ts|tsx)$/, + use: 'babel-loader', + }, + }, + }; + + const webpackResult = result.webpack!(mockWebpackConfig, mockWebpackOptions); + + expect(webpackResult.module?.rules).toContainEqual({ + test: new RegExp('/demos/[^/]+/index\\.ts$'), + use: [ + mockWebpackOptions.defaultLoaders.babel, + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', + options: { performance: performanceOptions, output: 'hastGzip' }, + }, + ], + }); + + expect(webpackResult.module?.rules).toContainEqual({ + test: new RegExp('/demos/[^/]+/client\\.ts$'), + use: [ + mockWebpackOptions.defaultLoaders.babel, + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: performanceOptions }, + }, + ], + }); + }); + + it('should pass performance options to additional demo patterns', () => { + const performanceOptions = { + logging: false, + notableMs: 200, + }; + + const plugin = withDocsInfra({ + performance: performanceOptions, + additionalDemoPatterns: { + index: ['./app/**/demos/*/demo-*/index.ts'], + client: ['./app/**/demos/*/demo-*/client.ts'], + }, + }); + const result = plugin({}); + + // Check turbopack rules include performance options + expect(result.turbopack?.rules?.['./app/**/demos/*/demo-*/index.ts']).toEqual({ + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', + options: { performance: performanceOptions, output: 'hastGzip' }, + }, + ], + }); + + expect(result.turbopack?.rules?.['./app/**/demos/*/demo-*/client.ts']).toEqual({ + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance: performanceOptions }, + }, + ], + }); + + // Check webpack rules include performance options + const mockWebpackConfig: WebpackConfig = { + module: { + rules: [], + }, + }; + + const mockWebpackOptions = { + buildId: 'test-build', + dev: false, + isServer: false, + config: {}, + defaultLoaders: { + babel: { + test: /\.(js|jsx|ts|tsx)$/, + use: 'babel-loader', + }, + }, + }; + + const webpackResult = result.webpack!(mockWebpackConfig, mockWebpackOptions); + + // Should have 4 rules total: 2 default + 2 additional demo patterns + expect(webpackResult.module?.rules).toHaveLength(4); + + // Check that additional patterns have performance options + const additionalIndexRule = webpackResult.module?.rules?.find((rule: any) => { + const source = rule.test?.source || rule.test?.toString(); + return source && source.includes('demo-') && source.includes('index'); + }) as any; + + const additionalClientRule = webpackResult.module?.rules?.find((rule: any) => { + const source = rule.test?.source || rule.test?.toString(); + return source && source.includes('demo-') && source.includes('client'); + }) as any; + + expect(additionalIndexRule?.use[1]?.options).toEqual({ + performance: performanceOptions, + output: 'hastGzip', + }); + expect(additionalClientRule?.use[1]?.options).toEqual({ performance: performanceOptions }); + }); + + it('should handle undefined performance options gracefully', () => { + const plugin = withDocsInfra(); // No performance options provided + const result = plugin({}); + + expect(result.turbopack?.rules?.['./app/**/demos/*/index.ts']).toEqual({ + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', + options: { performance: {}, output: 'hastGzip' }, + }, + ], + }); + }); + }); }); describe('getDocsInfraMdxOptions', () => { diff --git a/packages/docs-infra/src/withDocsInfra/withDocsInfra.ts b/packages/docs-infra/src/withDocsInfra/withDocsInfra.ts index cae0e1767..395f83044 100644 --- a/packages/docs-infra/src/withDocsInfra/withDocsInfra.ts +++ b/packages/docs-infra/src/withDocsInfra/withDocsInfra.ts @@ -7,7 +7,7 @@ export interface NextConfig { turbopack?: { rules?: Record< string, - { loaders: { loader: string; options: Record }[] | string[] } + { loaders: Array }> } >; }; webpack?: (config: WebpackConfig, options: WebpackOptions) => WebpackConfig; @@ -61,6 +61,14 @@ export interface WithDocsInfraOptions { * Additional Turbopack rules to merge with the default docs-infra rules. */ additionalTurbopackRules?: Record; + /** + * Performance logging options + */ + performance?: { + logging: boolean; + notableMs?: number; + showWrapperMeasures?: boolean; + }; /** * Defer AST parsing option for code highlighter output. * 'gzip' - Default, outputs gzipped HAST for best performance. @@ -129,6 +137,7 @@ export function withDocsInfra(options: WithDocsInfraOptions = {}) { clientDemoPathPattern = './app/**/demos/*/client.ts', additionalDemoPatterns = {}, additionalTurbopackRules = {}, + performance = {}, deferCodeParsing = 'gzip', } = options; @@ -152,12 +161,17 @@ export function withDocsInfra(options: WithDocsInfraOptions = {}) { loaders: [ { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output }, + options: { performance, output }, }, ], }, [clientDemoPathPattern]: { - loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'], + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance }, + }, + ], }, }; @@ -168,7 +182,7 @@ export function withDocsInfra(options: WithDocsInfraOptions = {}) { loaders: [ { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output }, + options: { performance, output }, }, ], }; @@ -178,7 +192,12 @@ export function withDocsInfra(options: WithDocsInfraOptions = {}) { if (additionalDemoPatterns.client) { additionalDemoPatterns.client.forEach((pattern) => { turbopackRules[pattern] = { - loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'], + loaders: [ + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance }, + }, + ], }; }); } @@ -220,7 +239,7 @@ export function withDocsInfra(options: WithDocsInfraOptions = {}) { defaultLoaders.babel, { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output }, + options: { performance, output }, }, ], }); @@ -230,7 +249,10 @@ export function withDocsInfra(options: WithDocsInfraOptions = {}) { test: new RegExp('/demos/[^/]+/client\\.ts$'), use: [ defaultLoaders.babel, - '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance }, + }, ], }); @@ -251,7 +273,7 @@ export function withDocsInfra(options: WithDocsInfraOptions = {}) { defaultLoaders.babel, { loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter', - options: { output }, + options: { performance, output }, }, ], }); @@ -272,7 +294,10 @@ export function withDocsInfra(options: WithDocsInfraOptions = {}) { test: new RegExp(`${regexPattern}$`), use: [ defaultLoaders.babel, - '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + { + loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient', + options: { performance }, + }, ], }); });