Skip to content

Commit c199f64

Browse files
committed
Use worker_threads to index files in parallel.
1 parent f7eca6b commit c199f64

File tree

14 files changed

+334
-137
lines changed

14 files changed

+334
-137
lines changed

.idea/misc.xml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/webdoc-cli/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ async function main(argv: yargs.Argv) {
107107
}
108108

109109
try {
110-
parse(sourceFiles, documentTree);
110+
await parse(sourceFiles, documentTree);
111111
} catch (e) {
112112
// Make sure we get that API structure out so the user can debug the problem!
113113
if (config.opts && config.opts.export) {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// @flow
2+
3+
import EventEmitter from "events";
4+
import type {LanguageConfig} from "../types/LanguageIntegration";
5+
import {type SourceFile} from "@webdoc/types";
6+
import {type Symbol} from "../types/Symbol";
7+
import {Worker} from "worker_threads";
8+
import os from "os";
9+
import path from "path";
10+
11+
export type JobData = {
12+
jobId: number,
13+
jobLanguageIntegrationModule: string,
14+
jobFile: SourceFile,
15+
jobConfig: LanguageConfig,
16+
};
17+
18+
export type JobResult = {
19+
jobId: number,
20+
jobResult?: Symbol,
21+
jobError?: any,
22+
};
23+
24+
class IndexerWorker extends EventEmitter {
25+
onError: (ev: "error", ...args: any[]) => void;
26+
onMessage: (...args: any[]) => void;
27+
worker: Worker;
28+
29+
constructor(worker: Worker) {
30+
super();
31+
32+
this.onError = this.emit.bind(this, "error");
33+
this.onMessage = this.onMessageImpl.bind(this);
34+
35+
this.worker = worker;
36+
this.worker.on("error", this.onError);
37+
this.worker.on("message", this.onMessage);
38+
}
39+
40+
send(
41+
jobId: number,
42+
jobLanguageIntegrationModule: string,
43+
jobFile: SourceFile,
44+
jobConfig: LanguageConfig,
45+
): number {
46+
this.worker.postMessage(({
47+
jobId,
48+
jobLanguageIntegrationModule,
49+
jobFile,
50+
jobConfig,
51+
}: JobData));
52+
53+
return jobId;
54+
}
55+
56+
onMessageImpl({jobId, jobResult, jobError}: JobResult) {
57+
this.emit(`response-${jobId}`, jobResult, jobError);
58+
}
59+
60+
destroy() {
61+
this.worker.terminate();
62+
}
63+
}
64+
65+
export class IndexerWorkerPool {
66+
workers: IndexerWorker[];
67+
ptr: number = 0;
68+
69+
constructor(limit?: number) {
70+
const workerPoolSize = Math.min(os.cpus().length, limit || Infinity);
71+
const workers = new Array<Worker>(workerPoolSize);
72+
const workerPath = path.resolve(__dirname, "./worker.js");
73+
74+
for (let i = 0; i < workerPoolSize; i++) {
75+
workers[i] = new IndexerWorker(new Worker(workerPath));
76+
}
77+
78+
this.workers = workers;
79+
}
80+
81+
index(langIntegrationModule: string, file: SourceFile, config: LanguageConfig): Promise<Symbol> {
82+
return new Promise<Symbol>((resolve, reject) => {
83+
const jobId = this.ptr++;
84+
const worker = this.workers[jobId % this.workers.length];
85+
86+
worker.send(jobId, langIntegrationModule, file, config);
87+
worker.on(`response-${jobId}`, (jobResult: Symbol, jobError: any): void => {
88+
if (jobResult) resolve(jobResult);
89+
else reject(jobError);
90+
});
91+
});
92+
}
93+
94+
destroy() {
95+
for (const worker of this.workers) {
96+
worker.destroy();
97+
}
98+
this.workers.length = 0;
99+
}
100+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// @flow
2+
3+
import type {LanguageConfig, LanguageIntegration} from "../types/LanguageIntegration";
4+
import {IndexerWorkerPool} from "./IndexerWorkerPool";
5+
import {type SourceFile} from "@webdoc/types";
6+
import {type Symbol} from "../types/Symbol";
7+
import {parserLogger} from "../Logger";
8+
import path from "path";
9+
10+
declare var globalThis: any;
11+
12+
// File-extension -> LanguageIntegration mapping
13+
const languages: { [id: string]: LanguageIntegration } = {};
14+
15+
// Register a language-integration that will be used to generate a symbol-tree for files with its
16+
// file-extensions.
17+
export function register(lang: LanguageIntegration): void {
18+
for (const ext of lang.extensions) {
19+
if (languages[ext]) {
20+
parserLogger.warn("LanguageIntegration",
21+
`.${ext} file extension has already been registered`);
22+
}
23+
24+
languages[ext] = lang;
25+
}
26+
}
27+
28+
export async function run(files: SourceFile[], config: LanguageConfig): Promise<Symbol[]> {
29+
const symbolTrees: Array<Symbol> = new Array(files.length);
30+
const symbolIndexingOperations: Array<Promise<void>> = new Array(files.length);
31+
const workerPool = new IndexerWorkerPool();
32+
33+
for (let i = 0; i < files.length; i++) {
34+
const fileName = files[i].path;
35+
const extension = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length);
36+
37+
if (!(extension in languages)) {
38+
throw new Error(`.${extension} language is not registered with the Indexer!`);
39+
}
40+
41+
const file = {
42+
...files[i],
43+
path: path.resolve(globalThis.process.cwd(), files[i].path),
44+
};
45+
const lang = languages[extension].module;
46+
47+
symbolIndexingOperations[i] = workerPool.index(lang, file, config).then(
48+
(symbolTree: Symbol): void => {
49+
symbolTrees[i] = symbolTree;
50+
},
51+
);
52+
}
53+
54+
await Promise.all(symbolIndexingOperations);
55+
56+
workerPool.destroy();
57+
58+
return symbolTrees;
59+
}
60+
61+
export function process(
62+
file: string,
63+
source: SourceFile,
64+
config: LanguageConfig,
65+
): Symbol {
66+
const fileName = source.path;
67+
const lang = languages[fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length)];
68+
69+
if (!lang) {
70+
throw new Error(`.${lang} file language is not registered`);
71+
}
72+
73+
return lang.parse(file, source, config);
74+
}
75+
76+
export function lang(module: string): LanguageIntegration {
77+
module = module.replace("@webdoc/parser", "../../");
78+
79+
// $FlowFixMe
80+
return ((require(module): any).default: LanguageIntegration);
81+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// @flow
2+
3+
import * as Indexer from "./";
4+
import type {JobData, JobResult} from "./IndexerWorkerPool";
5+
import {isMainThread, parentPort} from "worker_threads";
6+
import fs from "fs";
7+
8+
function onMessage(data: JobData) {
9+
function onFileRead(err: ?Error, contents: string) {
10+
if (err) {
11+
return parentPort.postMessage(({
12+
jobId: data.jobId,
13+
jobError: err,
14+
}: JobResult));
15+
}
16+
17+
const lang = Indexer.lang(data.jobLanguageIntegrationModule);
18+
const symbolTree = lang.parse(
19+
contents,
20+
data.jobFile,
21+
data.jobConfig,
22+
);
23+
24+
parentPort.postMessage(({
25+
jobId: data.jobId,
26+
jobResult: symbolTree,
27+
}: JobResult));
28+
}
29+
30+
if (!data.jobFile.content) {
31+
fs.readFile(data.jobFile.path, "utf8", onFileRead);
32+
} else {
33+
onFileRead(null, data.jobFile.content);
34+
}
35+
}
36+
37+
if (!isMainThread) {
38+
parentPort.on("message", onMessage);
39+
}

packages/webdoc-parser/src/parse.js

Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,20 @@
11
// @flow
22

3-
import type {LanguageConfig, LanguageIntegration} from "./types/LanguageIntegration";
3+
import * as Indexer from "./indexer";
44
import type {RootDoc, SourceFile} from "@webdoc/types";
55
import {createPackageDoc, createRootDoc} from "@webdoc/model";
66
import {langJS, langTS} from "./symbols-babel";
7+
import type {LanguageConfig} from "./types/LanguageIntegration";
78
import type {Symbol} from "./types/Symbol";
89
import assemble from "./assembler";
9-
import fs from "fs";
1010
import mod from "./transformer/document-tree-modifiers";
11-
import {parserLogger} from "./Logger";
12-
import path from "path";
1311
import transform from "./transformer";
1412

1513
declare var Webdoc: any;
1614

17-
// File-extension -> LanguageIntegration mapping
18-
const languages: { [id: string]: LanguageIntegration } = {};
19-
20-
// Register a language-integration that will be used to generate a symbol-tree for files with its
21-
// file-extensions.
22-
export function registerLanguage(lang: LanguageIntegration): void {
23-
for (const ext of lang.extensions) {
24-
if (languages[ext]) {
25-
parserLogger.warn("LanguageIntegration",
26-
`.${ext} file extension has already been registered`);
27-
}
28-
29-
languages[ext] = lang;
30-
}
31-
}
32-
3315
// Register built-in languages
34-
registerLanguage(langJS);
35-
registerLanguage(langTS);
16+
Indexer.register(langJS);
17+
Indexer.register(langTS);
3618

3719
// Default language-config for parsing documentation
3820
const DEFAULT_LANG_CONFIG: LanguageConfig = {
@@ -79,14 +61,7 @@ export function buildSymbolTree(
7961
};
8062
}
8163

82-
const fileName = source.path;
83-
const lang = languages[fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length)];
84-
85-
if (!lang) {
86-
throw new Error(`.${lang} file language is not registered`);
87-
}
88-
89-
return lang.parse(file, source, config);
64+
return Indexer.process(file, source, config);
9065
}
9166

9267
// TODO: Asynchronous API for parsing
@@ -107,7 +82,10 @@ export function buildSymbolTree(
10782
* @param {RootDoc} root
10883
* @return {RootDoc}
10984
*/
110-
export function parse(target: string | SourceFile[], root?: RootDoc = createRootDoc()): RootDoc {
85+
export async function parse(
86+
target: string | SourceFile[],
87+
root?: RootDoc = createRootDoc(),
88+
): Promise<RootDoc> {
11189
if (typeof target === "string") {
11290
target = [{
11391
content: target,
@@ -116,23 +94,12 @@ export function parse(target: string | SourceFile[], root?: RootDoc = createRoot
11694
}];
11795
}
11896

119-
const partialDoctrees = new Array(Array.isArray(target) ? target.length : target.size);
120-
121-
// Build a symbol-tree for all the files
122-
for (let i = 0; i < target.length; i++) {
123-
const {content, path: source} = target[i];
124-
125-
partialDoctrees[i] = buildSymbolTree(
126-
content || fs.readFileSync(path.join(process.cwd(), source), "utf8"),
127-
target[i],
128-
);
129-
}
130-
131-
const rsym = assemble(partialDoctrees);
97+
const perFileSymbolTrees = await Indexer.run(target, Webdoc.DEFAULT_LANG_CONFIG);
98+
const fullSymbolTree = assemble(perFileSymbolTrees);
13299

133100
root.children = root.members;
134101

135-
transform(rsym, root);
102+
transform(fullSymbolTree, root);
136103
mod(root);
137104

138105
return root;

packages/webdoc-parser/src/symbols-babel/index.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// This file provides langJS and langTS integration, both of which use Babel to generate
44
// symbols trees. These are registered in parse.js!
55

6-
import type {LanguageConfig} from "../types/LanguageIntegration";
6+
import type {LanguageConfig, LanguageIntegration} from "../types/LanguageIntegration";
77
import type {SourceFile} from "@webdoc/types";
88
import type {Symbol} from "../types/Symbol";
99
import buildSymbolTree from "./build-symbol-tree";
@@ -46,8 +46,9 @@ const tsPreset = [
4646
"typescript",
4747
];
4848

49-
export const langJS = {
49+
export const langJS: LanguageIntegration = {
5050
extensions: ["js", "jsx", "jsdoc"],
51+
module: "@webdoc/parser/lib/symbols-babel/langJS",
5152
parse(file: string, source: SourceFile, config: LanguageConfig): Symbol {
5253
mode.current = "flow";
5354

@@ -56,8 +57,9 @@ export const langJS = {
5657
},
5758
};
5859

59-
export const langTS = {
60+
export const langTS: LanguageIntegration = {
6061
extensions: ["ts", "tsx"],
62+
module: "@webdoc/parser/lib/symbols-babel/langTS",
6163
parse(file: string, source: SourceFile, config: LanguageConfig): Symbol {
6264
mode.current = "typescript";
6365

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {langJS as default} from "./";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {langTS as default} from "./";

packages/webdoc-parser/src/types/LanguageIntegration.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {Symbol} from "./Symbol";
66
// Language-integration definition - used for parsing code into a symbol-tree
77
export type LanguageIntegration = {
88
extensions: string[],
9+
module: string,
910
parse(file: string, source: SourceFile, config: LanguageConfig): Symbol
1011
};
1112

0 commit comments

Comments
 (0)