diff --git a/.changeset/silent-bags-sell.md b/.changeset/silent-bags-sell.md new file mode 100644 index 00000000000..43601b3bb8e --- /dev/null +++ b/.changeset/silent-bags-sell.md @@ -0,0 +1,8 @@ +--- +'@graphql-cli/codegen': minor +'@graphql-codegen/cli': minor +'@graphql-codegen/core': minor +'@graphql-codegen/plugin-helpers': minor +--- + +Performance Profiler --profile diff --git a/packages/graphql-codegen-cli/src/codegen.ts b/packages/graphql-codegen-cli/src/codegen.ts index 93a135a7301..4d4577eeaeb 100644 --- a/packages/graphql-codegen-cli/src/codegen.ts +++ b/packages/graphql-codegen-cli/src/codegen.ts @@ -52,20 +52,6 @@ function createCache(loader: (key: string) => Promise) { } export async function executeCodegen(input: CodegenContext | Types.Config): Promise { - function wrapTask(task: () => void | Promise, source: string) { - return async () => { - try { - await Promise.resolve().then(() => task()); - } catch (error) { - if (source && !(error instanceof GraphQLError)) { - error.source = source; - } - - throw error; - } - }; - } - const context = ensureContext(input); const config = context.getConfig(); const pluginContext = context.getPluginContext(); @@ -117,6 +103,21 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom documents: documents, }; }); + function wrapTask(task: () => void | Promise, source: string, taskName: string) { + return () => { + return context.profiler.run(async () => { + try { + await Promise.resolve().then(() => task()); + } catch (error) { + if (source && !(error instanceof GraphQLError)) { + error.source = source; + } + + throw error; + } + }, taskName); + }; + } async function normalize() { /* Load Require extensions */ @@ -232,119 +233,137 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom [ { title: 'Load GraphQL schemas', - task: wrapTask(async () => { - debugLog(`[CLI] Loading Schemas`); - - const schemaPointerMap: any = {}; - const allSchemaUnnormalizedPointers = [...rootSchemas, ...outputSpecificSchemas]; - for (const unnormalizedPtr of allSchemaUnnormalizedPointers) { - if (typeof unnormalizedPtr === 'string') { - schemaPointerMap[unnormalizedPtr] = {}; - } else if (typeof unnormalizedPtr === 'object') { - Object.assign(schemaPointerMap, unnormalizedPtr); + task: wrapTask( + async () => { + debugLog(`[CLI] Loading Schemas`); + + const schemaPointerMap: any = {}; + const allSchemaUnnormalizedPointers = [...rootSchemas, ...outputSpecificSchemas]; + for (const unnormalizedPtr of allSchemaUnnormalizedPointers) { + if (typeof unnormalizedPtr === 'string') { + schemaPointerMap[unnormalizedPtr] = {}; + } else if (typeof unnormalizedPtr === 'object') { + Object.assign(schemaPointerMap, unnormalizedPtr); + } } - } - const hash = JSON.stringify(schemaPointerMap); - const result = await schemaLoadingCache.load(hash); + const hash = JSON.stringify(schemaPointerMap); + const result = await schemaLoadingCache.load(hash); - outputSchemaAst = await result.outputSchemaAst; - outputSchema = result.outputSchema; - }, filename), + outputSchemaAst = await result.outputSchemaAst; + outputSchema = result.outputSchema; + }, + filename, + `Load GraphQL schemas: ${filename}` + ), }, { title: 'Load GraphQL documents', - task: wrapTask(async () => { - debugLog(`[CLI] Loading Documents`); + task: wrapTask( + async () => { + debugLog(`[CLI] Loading Documents`); - // get different cache for shared docs and output specific docs - const results = await Promise.all( - [rootDocuments, outputSpecificDocuments].map(docs => { - const hash = JSON.stringify(docs); - return documentsLoadingCache.load(hash); - }) - ); + // get different cache for shared docs and output specific docs + const results = await Promise.all( + [rootDocuments, outputSpecificDocuments].map(docs => { + const hash = JSON.stringify(docs); + return documentsLoadingCache.load(hash); + }) + ); - const documents: Types.DocumentFile[] = []; + const documents: Types.DocumentFile[] = []; - results.forEach(source => documents.push(...source.documents)); + results.forEach(source => documents.push(...source.documents)); - if (documents.length > 0) { - outputDocuments.push(...documents); - } - }, filename), + if (documents.length > 0) { + outputDocuments.push(...documents); + } + }, + filename, + `Load GraphQL documents: ${filename}` + ), }, { title: 'Generate', - task: wrapTask(async () => { - debugLog(`[CLI] Generating output`); - - const normalizedPluginsArray = normalizeConfig(outputConfig.plugins); - const pluginLoader = config.pluginLoader || makeDefaultLoader(context.cwd); - const pluginPackages = await Promise.all( - normalizedPluginsArray.map(plugin => getPluginByName(Object.keys(plugin)[0], pluginLoader)) - ); - const pluginMap: { [name: string]: CodegenPlugin } = {}; - const preset: Types.OutputPreset = hasPreset - ? typeof outputConfig.preset === 'string' - ? await getPresetByName(outputConfig.preset, makeDefaultLoader(context.cwd)) - : outputConfig.preset - : null; - - pluginPackages.forEach((pluginPackage, i) => { - const plugin = normalizedPluginsArray[i]; - const name = Object.keys(plugin)[0]; - - pluginMap[name] = pluginPackage; - }); - - const mergedConfig = { - ...rootConfig, - ...(typeof outputFileTemplateConfig === 'string' - ? { value: outputFileTemplateConfig } - : outputFileTemplateConfig), - }; - - let outputs: Types.GenerateOptions[] = []; - - if (hasPreset) { - outputs = await preset.buildGeneratesSection({ - baseOutputDir: filename, - presetConfig: outputConfig.presetConfig || {}, - plugins: normalizedPluginsArray, - schema: outputSchema, - schemaAst: outputSchemaAst, - documents: outputDocuments, - config: mergedConfig, - pluginMap, - pluginContext, + task: wrapTask( + async () => { + debugLog(`[CLI] Generating output`); + + const normalizedPluginsArray = normalizeConfig(outputConfig.plugins); + const pluginLoader = config.pluginLoader || makeDefaultLoader(context.cwd); + const pluginPackages = await Promise.all( + normalizedPluginsArray.map(plugin => getPluginByName(Object.keys(plugin)[0], pluginLoader)) + ); + const pluginMap: { [name: string]: CodegenPlugin } = {}; + const preset: Types.OutputPreset = hasPreset + ? typeof outputConfig.preset === 'string' + ? await getPresetByName(outputConfig.preset, makeDefaultLoader(context.cwd)) + : outputConfig.preset + : null; + + pluginPackages.forEach((pluginPackage, i) => { + const plugin = normalizedPluginsArray[i]; + const name = Object.keys(plugin)[0]; + + pluginMap[name] = pluginPackage; }); - } else { - outputs = [ - { - filename, - plugins: normalizedPluginsArray, - schema: outputSchema, - schemaAst: outputSchemaAst, - documents: outputDocuments, - config: mergedConfig, - pluginMap, - pluginContext, - }, - ]; - } - - const process = async (outputArgs: Types.GenerateOptions) => { - const output = await codegen(outputArgs); - result.push({ - filename: outputArgs.filename, - content: output, - hooks: outputConfig.hooks || {}, - }); - }; - await Promise.all(outputs.map(process)); - }, filename), + const mergedConfig = { + ...rootConfig, + ...(typeof outputFileTemplateConfig === 'string' + ? { value: outputFileTemplateConfig } + : outputFileTemplateConfig), + }; + + let outputs: Types.GenerateOptions[] = []; + + if (hasPreset) { + outputs = await context.profiler.run( + async () => + preset.buildGeneratesSection({ + baseOutputDir: filename, + presetConfig: outputConfig.presetConfig || {}, + plugins: normalizedPluginsArray, + schema: outputSchema, + schemaAst: outputSchemaAst, + documents: outputDocuments, + config: mergedConfig, + pluginMap, + pluginContext, + profiler: context.profiler, + }), + `Build Generates Section: ${filename}` + ); + } else { + outputs = [ + { + filename, + plugins: normalizedPluginsArray, + schema: outputSchema, + schemaAst: outputSchemaAst, + documents: outputDocuments, + config: mergedConfig, + pluginMap, + pluginContext, + profiler: context.profiler, + }, + ]; + } + + const process = async (outputArgs: Types.GenerateOptions) => { + const output = await codegen(outputArgs); + result.push({ + filename: outputArgs.filename, + content: output, + hooks: outputConfig.hooks || {}, + }); + }; + + await context.profiler.run(() => Promise.all(outputs.map(process)), `Codegen: ${filename}`); + }, + filename, + `Generate: ${filename}` + ), }, ], { diff --git a/packages/graphql-codegen-cli/src/config.ts b/packages/graphql-codegen-cli/src/config.ts index 8299b2b88a4..ea054400b7f 100644 --- a/packages/graphql-codegen-cli/src/config.ts +++ b/packages/graphql-codegen-cli/src/config.ts @@ -1,6 +1,6 @@ import { cosmiconfig, defaultLoaders } from 'cosmiconfig'; import { resolve } from 'path'; -import { DetailedError, Types } from '@graphql-codegen/plugin-helpers'; +import { DetailedError, Types, Profiler, createProfiler, createNoopProfiler } from '@graphql-codegen/plugin-helpers'; import { env } from 'string-env-interpolation'; import yargs from 'yargs'; import { GraphQLConfig } from 'graphql-config'; @@ -21,6 +21,7 @@ export type YamlCliFlags = { project: string; silent: boolean; errorsOnly: boolean; + profile: boolean; }; export function generateSearchPlaces(moduleName: string) { @@ -214,6 +215,10 @@ export function buildOptions() { describe: 'Only print errors', type: 'boolean' as const, }, + profile: { + describe: 'Use profiler to measure performance', + type: 'boolean' as const, + }, p: { alias: 'project', describe: 'Name of a project in GraphQL Config', @@ -272,6 +277,10 @@ export function updateContextWithCliFlags(context: CodegenContext, cliFlags: Yam context.useProject(cliFlags.project); } + if (cliFlags.profile === true) { + context.useProfiler(); + } + context.updateConfig(config); } @@ -281,8 +290,11 @@ export class CodegenContext { private config: Types.Config; private _project?: string; private _pluginContext: { [key: string]: any } = {}; + cwd: string; filepath: string; + profiler: Profiler; + profilerOutput?: string; constructor({ config, @@ -297,6 +309,7 @@ export class CodegenContext { this._graphqlConfig = graphqlConfig; this.filepath = this._graphqlConfig ? this._graphqlConfig.filepath : filepath; this.cwd = this._graphqlConfig ? this._graphqlConfig.dirpath : process.cwd(); + this.profiler = createNoopProfiler(); } useProject(name?: string) { @@ -332,6 +345,16 @@ export class CodegenContext { }; } + useProfiler() { + this.profiler = createProfiler(); + + const now = new Date(); // 2011-10-05T14:48:00.000Z + const datetime = now.toISOString().split('.')[0]; // 2011-10-05T14:48:00 + const datetimeNormalized = datetime.replace(/-|:/g, ''); // 20111005T144800 + + this.profilerOutput = `codegen-${datetimeNormalized}.json`; + } + getPluginContext(): { [key: string]: any } { return this._pluginContext; } diff --git a/packages/graphql-codegen-cli/src/generate-and-save.ts b/packages/graphql-codegen-cli/src/generate-and-save.ts index 7359c855e50..f4e6976e228 100644 --- a/packages/graphql-codegen-cli/src/generate-and-save.ts +++ b/packages/graphql-codegen-cli/src/generate-and-save.ts @@ -17,7 +17,7 @@ export async function generate( ): Promise { const context = ensureContext(input); const config = context.getConfig(); - await lifecycleHooks(config.hooks).afterStart(); + await context.profiler.run(() => lifecycleHooks(config.hooks).afterStart(), 'Lifecycle: afterStart'); let previouslyGeneratedFilenames: string[] = []; function removeStaleFiles(config: Types.Config, generationResult: Types.FileOutput[]) { @@ -49,49 +49,59 @@ export async function generate( removeStaleFiles(config, generationResult); } - await lifecycleHooks(config.hooks).beforeAllFileWrite(generationResult.map(r => r.filename)); - - await Promise.all( - generationResult.map(async (result: Types.FileOutput) => { - const exists = fileExists(result.filename); - - if (!shouldOverwrite(config, result.filename) && exists) { - return; - } - - const content = result.content || ''; - const currentHash = hash(content); - let previousHash = recentOutputHash.get(result.filename); - - if (!previousHash && exists) { - previousHash = hash(readSync(result.filename)); - } - - if (previousHash && currentHash === previousHash) { - debugLog(`Skipping file (${result.filename}) writing due to indentical hash...`); - - return; - } - - if (content.length === 0) { - return; - } - - recentOutputHash.set(result.filename, currentHash); - const basedir = dirname(result.filename); - await lifecycleHooks(result.hooks).beforeOneFileWrite(result.filename); - await lifecycleHooks(config.hooks).beforeOneFileWrite(result.filename); - mkdirp.sync(basedir); - const absolutePath = isAbsolute(result.filename) - ? result.filename - : join(input.cwd || process.cwd(), result.filename); - writeSync(absolutePath, result.content); - await lifecycleHooks(result.hooks).afterOneFileWrite(result.filename); - await lifecycleHooks(config.hooks).afterOneFileWrite(result.filename); - }) + await context.profiler.run( + () => lifecycleHooks(config.hooks).beforeAllFileWrite(generationResult.map(r => r.filename)), + 'Lifecycle: beforeAllFileWrite' ); - await lifecycleHooks(config.hooks).afterAllFileWrite(generationResult.map(r => r.filename)); + await context.profiler.run( + () => + Promise.all( + generationResult.map(async (result: Types.FileOutput) => { + const exists = fileExists(result.filename); + + if (!shouldOverwrite(config, result.filename) && exists) { + return; + } + + const content = result.content || ''; + const currentHash = hash(content); + let previousHash = recentOutputHash.get(result.filename); + + if (!previousHash && exists) { + previousHash = hash(readSync(result.filename)); + } + + if (previousHash && currentHash === previousHash) { + debugLog(`Skipping file (${result.filename}) writing due to identical hash...`); + + return; + } + + if (content.length === 0) { + return; + } + + recentOutputHash.set(result.filename, currentHash); + const basedir = dirname(result.filename); + await lifecycleHooks(result.hooks).beforeOneFileWrite(result.filename); + await lifecycleHooks(config.hooks).beforeOneFileWrite(result.filename); + mkdirp.sync(basedir); + const absolutePath = isAbsolute(result.filename) + ? result.filename + : join(input.cwd || process.cwd(), result.filename); + writeSync(absolutePath, result.content); + await lifecycleHooks(result.hooks).afterOneFileWrite(result.filename); + await lifecycleHooks(config.hooks).afterOneFileWrite(result.filename); + }) + ), + 'Write files' + ); + + await context.profiler.run( + () => lifecycleHooks(config.hooks).afterAllFileWrite(generationResult.map(r => r.filename)), + 'Lifecycle: afterAllFileWrite' + ); return generationResult; } @@ -101,11 +111,14 @@ export async function generate( return createWatcher(context, writeOutput); } - const outputFiles = await executeCodegen(context); + const outputFiles = await context.profiler.run(() => executeCodegen(context), 'executeCodegen'); - await writeOutput(outputFiles); + await context.profiler.run(() => writeOutput(outputFiles), 'writeOutput'); + await context.profiler.run(() => lifecycleHooks(config.hooks).beforeDone(), 'Lifecycle: beforeDone'); - lifecycleHooks(config.hooks).beforeDone(); + if (context.profilerOutput) { + writeSync(join(context.cwd, context.profilerOutput), JSON.stringify(context.profiler.collect())); + } return outputFiles; } diff --git a/packages/graphql-codegen-core/src/codegen.ts b/packages/graphql-codegen-core/src/codegen.ts index bf354581788..fdb658afd5d 100644 --- a/packages/graphql-codegen-core/src/codegen.ts +++ b/packages/graphql-codegen-core/src/codegen.ts @@ -5,6 +5,7 @@ import { federationSpec, getCachedDocumentNodeFromSchema, AddToSchemaResult, + createNoopProfiler, } from '@graphql-codegen/plugin-helpers'; import { visit, DefinitionNode, Kind, print, NameNode, specifiedRules, DocumentNode } from 'graphql'; import { executePlugin } from './execute-plugin'; @@ -22,11 +23,12 @@ import { export async function codegen(options: Types.GenerateOptions): Promise { const documents = options.documents || []; + const profiler = options.profiler ?? createNoopProfiler(); const skipDocumentsValidation = getSkipDocumentsValidationOption(options); if (documents.length > 0 && shouldValidateDuplicateDocuments(skipDocumentsValidation)) { - validateDuplicateDocuments(documents); + await profiler.run(async () => validateDuplicateDocuments(documents), 'validateDuplicateDocuments'); } const pluginPackages = Object.keys(options.pluginMap).map(key => options.pluginMap[key]); @@ -52,18 +54,20 @@ export async function codegen(options: Types.GenerateOptions): Promise { // Use mergeSchemas, only if there is no GraphQLSchema provided or the schema should be extended const mergeNeeded = !options.schemaAst || additionalTypeDefs.length > 0; - const schemaInstance = mergeNeeded - ? mergeSchemas({ - // If GraphQLSchema provided, use it - schemas: options.schemaAst ? [options.schemaAst] : [], - // If GraphQLSchema isn't provided but DocumentNode is, use it to get the final GraphQLSchema - typeDefs: options.schemaAst ? additionalTypeDefs : [options.schema, ...additionalTypeDefs], - convertExtensions: true, - assumeValid: true, - assumeValidSDL: true, - ...options.config, - } as any) - : options.schemaAst; + const schemaInstance = await profiler.run(async () => { + return mergeNeeded + ? mergeSchemas({ + // If GraphQLSchema provided, use it + schemas: options.schemaAst ? [options.schemaAst] : [], + // If GraphQLSchema isn't provided but DocumentNode is, use it to get the final GraphQLSchema + typeDefs: options.schemaAst ? additionalTypeDefs : [options.schema, ...additionalTypeDefs], + convertExtensions: true, + assumeValid: true, + assumeValidSDL: true, + ...options.config, + } as any) + : options.schemaAst; + }, 'Create schema instance'); const schemaDocumentNode = mergeNeeded || !options.schema ? getCachedDocumentNodeFromSchema(schemaInstance) : options.schema; @@ -75,16 +79,21 @@ export async function codegen(options: Types.GenerateOptions): Promise { } const extraFragments: { importFrom: string; node: DefinitionNode }[] = pickFlag('externalFragments', options.config) || []; - const errors = await validateGraphQlDocuments( - schemaInstance, - [ - ...documents, - ...extraFragments.map(f => ({ - location: f.importFrom, - document: { kind: Kind.DOCUMENT, definitions: [f.node] } as DocumentNode, - })), - ], - specifiedRules.filter(rule => !ignored.some(ignoredRule => rule.name.startsWith(ignoredRule))) + + const errors = await profiler.run( + () => + validateGraphQlDocuments( + schemaInstance, + [ + ...documents, + ...extraFragments.map(f => ({ + location: f.importFrom, + document: { kind: Kind.DOCUMENT, definitions: [f.node] } as DocumentNode, + })), + ], + specifiedRules.filter(rule => !ignored.some(ignoredRule => rule.name.startsWith(ignoredRule))) + ), + 'Validate documents against schema' ); checkValidationErrors(errors); } @@ -106,20 +115,25 @@ export async function codegen(options: Types.GenerateOptions): Promise { ...pluginConfig, }; - const result = await executePlugin( - { - name, - config: execConfig, - parentConfig: options.config, - schema: schemaDocumentNode, - schemaAst: schemaInstance, - documents: options.documents, - outputFilename: options.filename, - allPlugins: options.plugins, - skipDocumentsValidation: options.skipDocumentsValidation, - pluginContext: options.pluginContext, - }, - pluginPackage + const result = await profiler.run( + () => + executePlugin( + { + name, + config: execConfig, + parentConfig: options.config, + schema: schemaDocumentNode, + schemaAst: schemaInstance, + documents: options.documents, + outputFilename: options.filename, + allPlugins: options.plugins, + skipDocumentsValidation: options.skipDocumentsValidation, + pluginContext: options.pluginContext, + profiler, + }, + pluginPackage + ), + `Plugin ${name}` ); if (typeof result === 'string') { diff --git a/packages/graphql-codegen-core/src/execute-plugin.ts b/packages/graphql-codegen-core/src/execute-plugin.ts index 5be9a807dd2..020cb967966 100644 --- a/packages/graphql-codegen-core/src/execute-plugin.ts +++ b/packages/graphql-codegen-core/src/execute-plugin.ts @@ -1,4 +1,4 @@ -import { DetailedError, Types, CodegenPlugin } from '@graphql-codegen/plugin-helpers'; +import { DetailedError, Types, CodegenPlugin, Profiler, createNoopProfiler } from '@graphql-codegen/plugin-helpers'; import { DocumentNode, GraphQLSchema, buildASTSchema } from 'graphql'; export interface ExecutePluginOptions { @@ -12,6 +12,7 @@ export interface ExecutePluginOptions { allPlugins: Types.ConfiguredPlugin[]; skipDocumentsValidation?: Types.SkipDocumentsValidationOptions; pluginContext?: { [key: string]: any }; + profiler?: Profiler; } export async function executePlugin(options: ExecutePluginOptions, plugin: CodegenPlugin): Promise { @@ -35,17 +36,22 @@ export async function executePlugin(options: ExecutePluginOptions, plugin: Codeg const outputSchema: GraphQLSchema = options.schemaAst || buildASTSchema(options.schema, options.config as any); const documents = options.documents || []; const pluginContext = options.pluginContext || {}; + const profiler = options.profiler ?? createNoopProfiler(); if (plugin.validate && typeof plugin.validate === 'function') { try { // FIXME: Sync validate signature with plugin signature - await plugin.validate( - outputSchema, - documents, - options.config, - options.outputFilename, - options.allPlugins, - pluginContext + await profiler.run( + async () => + plugin.validate( + outputSchema, + documents, + options.config, + options.outputFilename, + options.allPlugins, + pluginContext + ), + `Plugin ${options.name} validate` ); } catch (e) { throw new DetailedError( @@ -57,16 +63,20 @@ export async function executePlugin(options: ExecutePluginOptions, plugin: Codeg } } - return Promise.resolve( - plugin.plugin( - outputSchema, - documents, - typeof options.config === 'object' ? { ...options.config } : options.config, - { - outputFile: options.outputFilename, - allPlugins: options.allPlugins, - pluginContext, - } - ) + return profiler.run( + () => + Promise.resolve( + plugin.plugin( + outputSchema, + documents, + typeof options.config === 'object' ? { ...options.config } : options.config, + { + outputFile: options.outputFilename, + allPlugins: options.allPlugins, + pluginContext, + } + ) + ), + `Plugin ${options.name} execution` ); } diff --git a/packages/utils/plugins-helpers/src/index.ts b/packages/utils/plugins-helpers/src/index.ts index 13efe90dfdd..5009a33680a 100644 --- a/packages/utils/plugins-helpers/src/index.ts +++ b/packages/utils/plugins-helpers/src/index.ts @@ -6,3 +6,4 @@ export * from './federation'; export * from './errors'; export * from './getCachedDocumentNodeFromSchema'; export * from './oldVisit'; +export * from './profiler'; diff --git a/packages/utils/plugins-helpers/src/profiler.ts b/packages/utils/plugins-helpers/src/profiler.ts new file mode 100644 index 00000000000..cbeaebed3cd --- /dev/null +++ b/packages/utils/plugins-helpers/src/profiler.ts @@ -0,0 +1,80 @@ +export interface ProfilerEvent { + /** The name of the event, as displayed in Trace Viewer */ + name: string; + /** The event categories. This is a comma separated list of categories for the event. The categories can be used to hide events in the Trace Viewer UI. */ + cat: string; + /** The event type. This is a single character which changes depending on the type of event being output. The valid values are listed in the table below. We will discuss each phase type below. */ + ph: string; + /** The tracing clock timestamp of the event. The timestamps are provided at microsecond granularity. */ + ts: number; + /** Optional. The thread clock timestamp of the event. The timestamps are provided at microsecond granularity. */ + tts?: string; + /** The process ID for the process that output this event. */ + pid: number; + /** The thread ID for the thread that output this event. */ + tid: number; + /** Any arguments provided for the event. Some of the event types have required argument fields, otherwise, you can put any information you wish in here. The arguments are displayed in Trace Viewer when you view an event in the analysis section. */ + args?: any; + /** duration */ + dur: number; + /** A fixed color name to associate with the event. If provided, cname must be one of the names listed in trace-viewer's base color scheme's reserved color names list */ + cname?: string; +} + +export interface Profiler { + run(fn: () => Promise, name: string, cat?: string): Promise; + collect(): ProfilerEvent[]; +} + +export function createNoopProfiler(): Profiler { + return { + run(fn) { + return Promise.resolve().then(() => fn()); + }, + collect() { + return []; + }, + }; +} + +export function createProfiler(): Profiler { + const events: ProfilerEvent[] = []; + + return { + collect() { + return events; + }, + run(fn, name, cat) { + let startTime: [number, number]; + + return Promise.resolve() + .then(() => { + startTime = process.hrtime(); + }) + .then(() => fn()) + .then(value => { + const duration = process.hrtime(startTime); + + // Trace Event Format documentation: + // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview + const event: ProfilerEvent = { + name, + cat, + ph: 'X', + ts: hrtimeToMicroseconds(startTime), + pid: 1, + tid: 0, + dur: hrtimeToMicroseconds(duration), + }; + + events.push(event); + + return value; + }); + }, + }; +} + +function hrtimeToMicroseconds(hrtime: any) { + return (hrtime[0] * 1e9 + hrtime[1]) / 1000; +} diff --git a/packages/utils/plugins-helpers/src/types.ts b/packages/utils/plugins-helpers/src/types.ts index ee6a993a346..66a792db69d 100644 --- a/packages/utils/plugins-helpers/src/types.ts +++ b/packages/utils/plugins-helpers/src/types.ts @@ -1,5 +1,6 @@ import { GraphQLSchema, DocumentNode } from 'graphql'; import { Source } from '@graphql-tools/utils'; +import type { Profiler } from './profiler'; export namespace Types { export interface GenerateOptions { @@ -15,6 +16,7 @@ export namespace Types { }; skipDocumentsValidation?: Types.SkipDocumentsValidationOptions; pluginContext?: { [key: string]: any }; + profiler?: Profiler; } export type FileOutput = { @@ -319,6 +321,7 @@ export namespace Types { pluginContext?: { [name: string]: any; }; + profiler?: Profiler; }; export type OutputPreset = { diff --git a/website/docs/advanced/profiler.mdx b/website/docs/advanced/profiler.mdx new file mode 100644 index 00000000000..06105800214 --- /dev/null +++ b/website/docs/advanced/profiler.mdx @@ -0,0 +1,37 @@ +--- +id: profiler +title: GraphQL Code Generator Profiler +--- + + + +GraphQL Code Generator CLI provides a flag that enables the profiler mode, as follows: + + + + +GraphQL Code Generator operates as usual (generating your files) but also generates a `codegen-[timestamp].json` profile file. + +This profile file can be loaded into the Chrome Dev Tools as follows: + + +1. Open a new Chrome tab (or other browsers) and open the Chrome Dev Tools + +2. Click on the "Performance" tab + +3. Load the `codegen-[timestamp].json` file as follows: + +![Profiler](/assets/docs/codegen-profile-1.png) + + +4. You then have access to the graph view: + +![Profiler](/assets/docs/codegen-profile-2.png) + + +The graph view shows the time spent on the main tasks of the codegen. + +Inspecting it allows you to identify: + +- if your configuration could benefit from excluding some documents from loading +- if you are facing a bug that [should be reported](https://github.com/dotansimha/graphql-code-generator/issues) diff --git a/website/docs/config-reference/codegen-config.md b/website/docs/config-reference/codegen-config.md index e5abf7b88c5..87a1297d9fb 100644 --- a/website/docs/config-reference/codegen-config.md +++ b/website/docs/config-reference/codegen-config.md @@ -129,6 +129,8 @@ The Codegen also supports several CLI flags that allow you to override the defau - **`--overwrite` (`-o`)** - Overrides the `overwrite` config to true. +- **`--profile`** - Use profiler to measure performance. _(see "Profiler" in "Advanced Usage")_ + ## Debug Mode You can set the `DEBUG` environment variable to `1` in order to tell the codegen to print debug information. diff --git a/website/public/assets/docs/codegen-profile-1.png b/website/public/assets/docs/codegen-profile-1.png new file mode 100644 index 00000000000..1b2d9b4231c Binary files /dev/null and b/website/public/assets/docs/codegen-profile-1.png differ diff --git a/website/public/assets/docs/codegen-profile-2.png b/website/public/assets/docs/codegen-profile-2.png new file mode 100644 index 00000000000..48ad1cc3dd6 Binary files /dev/null and b/website/public/assets/docs/codegen-profile-2.png differ diff --git a/website/routes.ts b/website/routes.ts index 9be3cfd770e..f39901b58ad 100644 --- a/website/routes.ts +++ b/website/routes.ts @@ -43,6 +43,7 @@ export function getRoutes(): IRoutes { ['generated-files-colocation', 'Generated files colocation'], ['programmatic-usage', 'Programmatic Usage'], ['how-does-it-work', 'How does it work?'], + ['profiler', 'Profiler'], ], }, 'docs/integrations': {