Skip to content

Commit 6f38120

Browse files
committed
refactor multithreading
1 parent 482c598 commit 6f38120

4 files changed

Lines changed: 127 additions & 53 deletions

File tree

src/dictionary/build.ts

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
// this code is Deno only
22

3-
import { unreachable } from "@std/assert/unreachable";
43
import { extractResultError, ResultError } from "../compound.ts";
54
import { PositionedError } from "../parser/parser_lib.ts";
6-
import { HEADS, parseDictionary } from "./parser.ts";
5+
import { parseDictionary } from "./parallel_parser.ts";
76
import { Dictionary } from "./type.ts";
87

98
const SOURCE = new URL("../../dictionary.txt", import.meta.url);
@@ -28,69 +27,27 @@ export const dictionary: Dictionary = new Map(Object.entries(json));
2827
`;
2928
await Deno.writeTextFile(DESTINATION, code);
3029
}
31-
function buildOffloaded(src: string): Promise<Dictionary> {
32-
return new Promise((resolve, reject) => {
33-
const worker = new Worker(
34-
new URL("./worker.ts", import.meta.url),
35-
{ type: "module" },
36-
);
37-
worker.postMessage(src);
38-
worker.onmessage = (event) => {
39-
resolve(event.data as Dictionary);
40-
worker.terminate();
41-
};
42-
worker.onerror = (event) => {
43-
reject(event.error);
44-
};
45-
});
46-
}
4730
export async function build(): Promise<boolean> {
4831
// deno-lint-ignore no-console
4932
console.log(
5033
`Building dictionary with ${navigator.hardwareConcurrency} threads...`,
5134
);
5235
const start = performance.now();
5336
const text = await Deno.readTextFile(SOURCE);
54-
const heads = [...text.matchAll(HEADS)].map((match) => match.index);
55-
const regionIndices = [...new Array(navigator.hardwareConcurrency).keys()]
56-
.map((index) => {
57-
const start = index * text.length / navigator.hardwareConcurrency;
58-
for (const head of heads) {
59-
if (start <= head) {
60-
return head;
61-
}
62-
}
63-
});
64-
const regions = regionIndices.map((index, i) =>
65-
text.slice(index, regionIndices[i + 1] ?? text.length)
66-
);
67-
const dictionary: Dictionary = new Map();
37+
let dictionary: Dictionary;
6838
try {
69-
const entries = await Promise.all(
70-
regions.map((region) => buildOffloaded(region)),
71-
);
72-
for (const entry of entries) {
73-
for (const [name, definition] of entry) {
74-
if (dictionary.has(name)) {
75-
throw new Error();
76-
}
77-
dictionary.set(name, definition);
78-
}
79-
}
80-
} catch (_) {
81-
try {
82-
parseDictionary(text);
83-
} catch (error) {
84-
displayError(`${SOURCE}`, extractResultError(error));
85-
return false;
86-
}
87-
unreachable();
39+
dictionary = await parseDictionary(text);
40+
} catch (error) {
41+
displayError(text, extractResultError(error));
42+
return false;
8843
}
8944
await buildWithDictionary(dictionary);
9045
const end = performance.now();
9146
const total = Math.floor(end - start);
9247
// deno-lint-ignore no-console
93-
console.log(`Building dictionary done in ${total}ms`);
48+
console.log(
49+
`Building dictionary done in ${total}ms`,
50+
);
9451
return true;
9552
}
9653
function displayError(source: string, errors: ReadonlyArray<ResultError>) {

src/dictionary/parallel_parser.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { unreachable } from "@std/assert/unreachable";
2+
import { extractResultError, ResultError } from "../compound.ts";
3+
import { Position, PositionedError } from "../parser/parser_lib.ts";
4+
import { HEADS } from "./parser.ts";
5+
import { Dictionary } from "./type.ts";
6+
7+
type WorkerError =
8+
| Readonly<
9+
{
10+
type: "positioned error";
11+
errors: ReadonlyArray<Position & Readonly<{ message: string }>>;
12+
}
13+
>
14+
| Readonly<{ type: "other"; error: unknown }>;
15+
16+
function buildOffloaded(src: string): Promise<Dictionary> {
17+
return new Promise((resolve, reject) => {
18+
const worker = new Worker(
19+
new URL("./worker.ts", import.meta.url),
20+
{ type: "module" },
21+
);
22+
worker.postMessage(src);
23+
worker.onmessage = (event) => {
24+
resolve(event.data as Dictionary);
25+
worker.terminate();
26+
};
27+
worker.onerror = (event) => {
28+
const error = event.error as WorkerError;
29+
switch (error.type) {
30+
case "positioned error":
31+
reject(
32+
new AggregateError(
33+
error.errors.map((error) =>
34+
new PositionedError(error.message, error)
35+
),
36+
),
37+
);
38+
break;
39+
case "other":
40+
reject(error.error);
41+
break;
42+
}
43+
};
44+
});
45+
}
46+
export async function parseDictionary(src: string): Promise<Dictionary> {
47+
const heads = [
48+
...[...src.matchAll(HEADS)].map((match) => match.index),
49+
src.length,
50+
];
51+
const regionIndices = [...new Array(navigator.hardwareConcurrency).keys()]
52+
.map((index) => {
53+
const start = index * src.length / navigator.hardwareConcurrency;
54+
for (const head of heads) {
55+
if (start <= head) {
56+
return head;
57+
}
58+
}
59+
unreachable();
60+
});
61+
const jobs = regionIndices.map((index, i) => ({
62+
index: index,
63+
job: buildOffloaded(src.slice(index, regionIndices[i + 1] ?? src.length)),
64+
}));
65+
const dictionary: Dictionary = new Map();
66+
const errors: Array<ResultError> = [];
67+
for (const job of jobs) {
68+
let entries: Dictionary;
69+
try {
70+
// deno-lint-ignore no-await-in-loop
71+
entries = await job.job;
72+
} catch (error) {
73+
for (const resultError of extractResultError(error)) {
74+
if (
75+
resultError instanceof PositionedError && resultError.position != null
76+
) {
77+
errors.push(
78+
new PositionedError(resultError.message, {
79+
position: job.index + resultError.position.position,
80+
length: resultError.position.length,
81+
cause: resultError,
82+
}),
83+
);
84+
} else {
85+
errors.push(resultError);
86+
}
87+
}
88+
continue;
89+
}
90+
if (errors.length === 0) {
91+
for (const [word, definition] of entries.entries()) {
92+
if (dictionary.has(word)) {
93+
errors.push(new ResultError(`duplicate Toki Pona word "${word}"`));
94+
break;
95+
} else {
96+
dictionary.set(word, definition);
97+
}
98+
}
99+
}
100+
}
101+
return dictionary;
102+
}

src/dictionary/worker.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1+
import { extractResultError, ResultError } from "../compound.ts";
12
import { parseDictionary } from "./parser.ts";
23

34
onmessage = (message) => {
4-
postMessage(parseDictionary(message.data as string));
5+
try {
6+
postMessage(parseDictionary(message.data as string));
7+
} catch (error) {
8+
let errors: ReadonlyArray<ResultError>;
9+
try {
10+
errors = extractResultError(error);
11+
} catch (error) {
12+
throw { type: "other", error };
13+
}
14+
throw {
15+
type: "positioned error",
16+
error: errors.map((error) => ({ ...error })),
17+
};
18+
}
519
};

src/parser/parser_lib.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class PositionedError extends ResultError {
106106
position: null | Position;
107107
constructor(
108108
message: string,
109+
// TODO: it's better to separate these two instead
109110
option?: Position & ErrorOption,
110111
) {
111112
super(message, option);

0 commit comments

Comments
 (0)