Skip to content

Commit 91df749

Browse files
authored
feat(perf): offload generators to worker threads (#247)
1 parent 104acfa commit 91df749

File tree

15 files changed

+160
-70
lines changed

15 files changed

+160
-70
lines changed

bin/cli.mjs

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@
22

33
import { resolve } from 'node:path';
44
import process from 'node:process';
5+
import { cpus } from 'node:os';
56

67
import { Command, Option } from 'commander';
78

89
import { coerce } from 'semver';
910
import { DOC_NODE_CHANGELOG_URL, DOC_NODE_VERSION } from '../src/constants.mjs';
1011
import createGenerator from '../src/generators.mjs';
11-
import generators from '../src/generators/index.mjs';
12+
import { publicGenerators } from '../src/generators/index.mjs';
1213
import createLinter from '../src/linter/index.mjs';
1314
import reporters from '../src/linter/reporters/index.mjs';
1415
import rules from '../src/linter/rules/index.mjs';
1516
import createMarkdownLoader from '../src/loaders/markdown.mjs';
1617
import createMarkdownParser from '../src/parsers/markdown.mjs';
1718
import createNodeReleases from '../src/releases.mjs';
1819

19-
const availableGenerators = Object.keys(generators);
20+
const availableGenerators = Object.keys(publicGenerators);
2021

2122
const program = new Command();
2223

@@ -77,10 +78,16 @@ program
7778
.choices(Object.keys(reporters))
7879
.default('console')
7980
)
81+
.addOption(
82+
new Option(
83+
'-p, --threads <number>',
84+
'The maximum number of threads to use. Set to 1 to disable parallelism'
85+
).default(Math.max(1, cpus().length - 1))
86+
)
8087
.parse(process.argv);
8188

8289
/**
83-
* @typedef {keyof generators} Target A list of the available generator names.
90+
* @typedef {keyof publicGenerators} Target A list of the available generator names.
8491
*
8592
* @typedef {Object} Options
8693
* @property {Array<string>|string} input Specifies the glob/path for input files.
@@ -108,6 +115,7 @@ const {
108115
lintDryRun,
109116
gitRef,
110117
reporter,
118+
threads,
111119
} = program.opts();
112120

113121
const linter = createLinter(lintDryRun, disableRule);
@@ -142,6 +150,8 @@ if (target) {
142150
// An URL containing a git ref URL pointing to the commit or ref that was used
143151
// to generate the API docs. This is used to link to the source code of the
144152
gitRef,
153+
// How many threads should be used
154+
threads,
145155
});
146156
}
147157

src/generators.mjs

+15-15
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
'use strict';
22

3-
import publicGenerators from './generators/index.mjs';
4-
import astJs from './generators/ast-js/index.mjs';
5-
import oramaDb from './generators/orama-db/index.mjs';
6-
7-
const availableGenerators = {
8-
...publicGenerators,
9-
// This one is a little special since we don't want it to run unless we need
10-
// it and we also don't want it to be publicly accessible through the CLI.
11-
'ast-js': astJs,
12-
'orama-db': oramaDb,
13-
};
3+
import { allGenerators } from './generators/index.mjs';
4+
import WorkerPool from './threading/index.mjs';
145

156
/**
167
* @typedef {{ ast: GeneratorMetadata<ApiDocMetadataEntry, ApiDocMetadataEntry>}} AstGenerator The AST "generator" is a facade for the AST tree and it isn't really a generator
@@ -43,30 +34,39 @@ const createGenerator = markdownInput => {
4334
*/
4435
const cachedGenerators = { ast: Promise.resolve(markdownInput) };
4536

37+
const threadPool = new WorkerPool();
38+
4639
/**
4740
* Runs the Generator engine with the provided top-level input and the given generator options
4841
*
4942
* @param {GeneratorOptions} options The options for the generator runtime
5043
*/
51-
const runGenerators = async ({ generators, ...extra }) => {
44+
const runGenerators = async ({ generators, threads, ...extra }) => {
5245
// Note that this method is blocking, and will only execute one generator per-time
5346
// but it ensures all dependencies are resolved, and that multiple bottom-level generators
5447
// can reuse the already parsed content from the top-level/dependency generators
5548
for (const generatorName of generators) {
56-
const { dependsOn, generate } = availableGenerators[generatorName];
49+
const { dependsOn, generate } = allGenerators[generatorName];
5750

5851
// If the generator dependency has not yet been resolved, we resolve
5952
// the dependency first before running the current generator
6053
if (dependsOn && dependsOn in cachedGenerators === false) {
61-
await runGenerators({ ...extra, generators: [dependsOn] });
54+
await runGenerators({
55+
...extra,
56+
threads,
57+
generators: [dependsOn],
58+
});
6259
}
6360

6461
// Ensures that the dependency output gets resolved before we run the current
6562
// generator with its dependency output as the input
6663
const dependencyOutput = await cachedGenerators[dependsOn];
6764

6865
// Adds the current generator execution Promise to the cache
69-
cachedGenerators[generatorName] = generate(dependencyOutput, extra);
66+
cachedGenerators[generatorName] =
67+
threads < 2
68+
? generate(dependencyOutput, extra) // Run in main thread
69+
: threadPool.run(generatorName, dependencyOutput, threads, extra); // Offload to worker thread
7070
}
7171

7272
// Returns the value of the last generator of the current pipeline

src/generators/index.mjs

+9-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import legacyJsonAll from './legacy-json-all/index.mjs';
99
import addonVerify from './addon-verify/index.mjs';
1010
import apiLinks from './api-links/index.mjs';
1111
import oramaDb from './orama-db/index.mjs';
12+
import astJs from './ast-js/index.mjs';
1213

13-
export default {
14+
export const publicGenerators = {
1415
'json-simple': jsonSimple,
1516
'legacy-html': legacyHtml,
1617
'legacy-html-all': legacyHtmlAll,
@@ -21,3 +22,10 @@ export default {
2122
'api-links': apiLinks,
2223
'orama-db': oramaDb,
2324
};
25+
26+
export const allGenerators = {
27+
...publicGenerators,
28+
// This one is a little special since we don't want it to run unless we need
29+
// it and we also don't want it to be publicly accessible through the CLI.
30+
'ast-js': astJs,
31+
};

src/generators/json-simple/index.mjs

-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { join } from 'node:path';
66
import { remove } from 'unist-util-remove';
77

88
import createQueries from '../../utils/queries/index.mjs';
9-
import { getRemark } from '../../utils/remark.mjs';
109

1110
/**
1211
* This generator generates a simplified JSON version of the API docs and returns it as a string
@@ -35,9 +34,6 @@ export default {
3534
* @param {Partial<GeneratorOptions>} options
3635
*/
3736
async generate(input, options) {
38-
// Gets a remark processor for stringifying the AST tree into JSON
39-
const remarkProcessor = getRemark();
40-
4137
// Iterates the input (ApiDocMetadataEntry) and performs a few changes
4238
const mappedInput = input.map(node => {
4339
// Deep clones the content nodes to avoid affecting upstream nodes
@@ -50,12 +46,6 @@ export default {
5046
createQueries.UNIST.isHeading,
5147
]);
5248

53-
/**
54-
* For the JSON generate we want to transform the whole content into JSON
55-
* @returns {string} The stringified JSON version of the content
56-
*/
57-
content.toJSON = () => remarkProcessor.stringify(content);
58-
5949
return { ...node, content };
6050
});
6151

src/generators/legacy-html-all/index.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export default {
8686
.replace('__ID__', 'all')
8787
.replace(/__FILENAME__/g, 'all')
8888
.replace('__SECTION__', 'All')
89-
.replace(/__VERSION__/g, `v${version.toString()}`)
89+
.replace(/__VERSION__/g, `v${version.version}`)
9090
.replace(/__TOC__/g, tableOfContents.wrapToC(aggregatedToC))
9191
.replace(/__GTOC__/g, parsedSideNav)
9292
.replace('__CONTENT__', aggregatedContent)

src/generators/legacy-html/index.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ export default {
8484
*/
8585
const replaceTemplateValues = values => {
8686
const { api, added, section, version, toc, nav, content } = values;
87-
8887
return apiTemplate
8988
.replace('__ID__', api)
9089
.replace(/__FILENAME__/g, api)
@@ -139,7 +138,7 @@ export default {
139138
api: head.api,
140139
added: head.introduced_in ?? '',
141140
section: head.heading.data.name || apiAsHeading,
142-
version: `v${version.toString()}`,
141+
version: `v${version.version}`,
143142
toc: String(parsedToC),
144143
nav: String(activeSideNav),
145144
content: parsedContent,

src/generators/legacy-html/utils/buildDropdowns.mjs

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ const buildNavigation = navigationContents =>
6060
const buildVersions = (api, added, versions) => {
6161
// All Node.js versions that support the current API; If there's no "introduced_at" field,
6262
// we simply show all versions, as we cannot pinpoint the exact version
63+
const coercedMajor = major(coerceSemVer(added));
6364
const compatibleVersions = versions.filter(({ version }) =>
64-
added ? major(version) >= major(coerceSemVer(added)) : true
65+
added ? version.major >= coercedMajor : true
6566
);
6667

6768
// Parses the SemVer version into something we use for URLs and to display the Node.js version

src/generators/legacy-json/utils/buildSection.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const createSectionBuilder = () => {
5858
* @param {import('../types.d.ts').HierarchizedEntry} entry - The entry providing stability information.
5959
*/
6060
const parseStability = (section, nodes, { stability }) => {
61-
const stabilityInfo = stability.toJSON()?.[0];
61+
const stabilityInfo = stability.children.map(node => node.data)?.[0];
6262

6363
if (stabilityInfo) {
6464
section.stability = stabilityInfo.index;

src/generators/types.d.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { SemVer } from 'semver';
22
import type { ApiDocReleaseEntry } from '../types';
3-
import type availableGenerators from './index.mjs';
3+
import type { publicGenerators } from './index.mjs';
44

55
declare global {
66
// All available generators as an inferable type, to allow Generator interfaces
77
// to be type complete and runtime friendly within `runGenerators`
8-
export type AvailableGenerators = typeof availableGenerators;
8+
export type AvailableGenerators = typeof publicGenerators;
99

1010
// This is the runtime config passed to the API doc generators
1111
export interface GeneratorOptions {
@@ -36,6 +36,9 @@ declare global {
3636
// i.e. https://github.com/nodejs/node/tree/2cb1d07e0f6d9456438016bab7db4688ab354fd2
3737
// i.e. https://gitlab.com/someone/node/tree/HEAD
3838
gitRef: string;
39+
40+
// The number of threads the process is allowed to use
41+
threads: number;
3942
}
4043

4144
export interface GeneratorMetadata<I extends any, O extends any> {

src/linter/tests/fixtures/entries.mjs

-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
/**
2-
* Noop function.
3-
*
4-
* @returns {any}
5-
*/
6-
const noop = () => {};
7-
81
/**
92
* @type {ApiDocMetadataEntry}
103
*/
@@ -69,12 +62,10 @@ export const assertEntry = {
6962
slug: 'assert',
7063
type: 'property',
7164
},
72-
toJSON: noop,
7365
},
7466
stability: {
7567
type: 'root',
7668
children: [],
77-
toJSON: noop,
7869
},
7970
content: {
8071
type: 'root',

src/metadata.mjs

-11
Original file line numberDiff line numberDiff line change
@@ -140,17 +140,6 @@ const createMetadata = slugger => {
140140
internalMetadata.heading.data.type =
141141
type ?? internalMetadata.heading.data.type;
142142

143-
/**
144-
* Defines the toJSON method for the Heading AST node to be converted as JSON
145-
*/
146-
internalMetadata.heading.toJSON = () => internalMetadata.heading.data;
147-
148-
/**
149-
* Maps the Stability Index AST nodes into a JSON objects from their data properties
150-
*/
151-
internalMetadata.stability.toJSON = () =>
152-
internalMetadata.stability.children.map(node => node.data);
153-
154143
// Returns the Metadata entry for the API doc
155144
return {
156145
api: apiDoc.stem,

src/test/metadata.test.mjs

+9-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ describe('createMetadata', () => {
3333
};
3434
metadata.addStability(stability);
3535
const actual = metadata.create(new VFile(), {}).stability;
36-
delete actual.toJSON;
3736
deepStrictEqual(actual, {
3837
children: [stability],
3938
type: 'root',
@@ -82,8 +81,15 @@ describe('createMetadata', () => {
8281
yaml_position: {},
8382
};
8483
const actual = metadata.create(apiDoc, section);
85-
delete actual.stability.toJSON;
86-
delete actual.heading.toJSON;
8784
deepStrictEqual(actual, expected);
8885
});
86+
87+
it('should be serializable', () => {
88+
const { create } = createMetadata(new GitHubSlugger());
89+
const actual = create(new VFile({ path: 'test.md' }), {
90+
type: 'root',
91+
children: [],
92+
});
93+
deepStrictEqual(structuredClone(actual), actual);
94+
});
8995
});

0 commit comments

Comments
 (0)