Use worker threads for sudoku generation
Moves sudoku generation off of the main thread. Allows for multiple generation requests (up to the number of physical CPU cores) to be served in parallel. Uses the "physical-cpu-count" node package to determine the numer of physical CPUs of the host. Also introduces a timeout which causes too-difficult (or impossible) generation requests to fail if they take more than 5 seconds to complete. In effect soft-capping the complexity of generated puzzles.
This commit is contained in:
parent
7f9cffbfdd
commit
7ac3e6ad06
20
package-lock.json
generated
20
package-lock.json
generated
@ -276,7 +276,14 @@
|
|||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "14.14.31",
|
"version": "14.14.31",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
|
||||||
"integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g=="
|
"integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/physical-cpu-count": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/physical-cpu-count/-/physical-cpu-count-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-kSK757BMb0j/7uP8+UR2wQ7GqrF9+Zk99XvAG4K/sL1VCFEqFoKic3xRHUttsGuTR0WMC3BjOTrf6dUIU+2CmQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.9.5",
|
"version": "6.9.5",
|
||||||
@ -304,6 +311,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.0.tgz",
|
||||||
"integrity": "sha512-BRR23Q9CJduH7AM6mk4JRttd8XyFkb4qIPZu4mdLF+VoP+wcjIxIWIKiBbN78NBbEuynrAyMPtzOHnIp2B/JPQ==",
|
"integrity": "sha512-BRR23Q9CJduH7AM6mk4JRttd8XyFkb4qIPZu4mdLF+VoP+wcjIxIWIKiBbN78NBbEuynrAyMPtzOHnIp2B/JPQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@ -1336,6 +1344,11 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node-worker-threads-pool": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-worker-threads-pool/-/node-worker-threads-pool-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-US55ZGzEDQY2oq8Bc33dFVNKGpx4KaCJqThMDomSsUeX8tMdp2eDjQ6OP0yFd1HTEuHuLqxXSTWC4eidEsbXlg=="
|
||||||
|
},
|
||||||
"nodemon": {
|
"nodemon": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz",
|
||||||
@ -1450,6 +1463,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz",
|
||||||
"integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg=="
|
"integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg=="
|
||||||
},
|
},
|
||||||
|
"physical-cpu-count": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/physical-cpu-count/-/physical-cpu-count-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-GN4vl+S/epVRrXURlCtUlverpmA="
|
||||||
|
},
|
||||||
"picomatch": {
|
"picomatch": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"koa": "^2.13.1",
|
"koa": "^2.13.1",
|
||||||
"koa-bodyparser": "^4.3.0",
|
"koa-bodyparser": "^4.3.0",
|
||||||
"koa-router": "^9.4.0",
|
"koa-router": "^9.4.0",
|
||||||
|
"node-worker-threads-pool": "^1.4.3",
|
||||||
|
"physical-cpu-count": "^2.0.0",
|
||||||
"stoppable": "^1.1.0"
|
"stoppable": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -20,6 +22,7 @@
|
|||||||
"@types/koa-bodyparser": "^4.3.0",
|
"@types/koa-bodyparser": "^4.3.0",
|
||||||
"@types/koa-router": "^7.4.1",
|
"@types/koa-router": "^7.4.1",
|
||||||
"@types/markdown-it": "^10.0.3",
|
"@types/markdown-it": "^10.0.3",
|
||||||
|
"@types/physical-cpu-count": "^2.0.0",
|
||||||
"@types/stoppable": "^1.1.0",
|
"@types/stoppable": "^1.1.0",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { gql } from "../mods";
|
import { gql } from "../mods";
|
||||||
import { generate } from "../sudoku/index";
|
import { generate, GenerateArguments } from "../sudoku/index";
|
||||||
|
|
||||||
export const typeDefs = gql`
|
export const typeDefs = gql`
|
||||||
type Cell {
|
type Cell {
|
||||||
@ -37,12 +37,6 @@ export const typeDefs = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type GenerateArguments = {
|
|
||||||
regionWidth: number;
|
|
||||||
regionHeight: number;
|
|
||||||
clues: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolvers = {
|
export const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
generate: (
|
generate: (
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { SudokuMath } from "./math";
|
import { SudokuMath } from "./math";
|
||||||
|
import { StaticPool, isTimeoutError } from "node-worker-threads-pool";
|
||||||
|
|
||||||
|
import WORKERS from "physical-cpu-count";
|
||||||
|
const TIMEOUT = 5000;
|
||||||
|
|
||||||
export type Cell = { value: number | null };
|
export type Cell = { value: number | null };
|
||||||
export type CellArray = Cell[];
|
export type CellArray = Cell[];
|
||||||
@ -7,6 +11,12 @@ export type Column = CellArray;
|
|||||||
export type Region = CellArray;
|
export type Region = CellArray;
|
||||||
export type Puzzle = CellArray;
|
export type Puzzle = CellArray;
|
||||||
|
|
||||||
|
export type GenerateArguments = {
|
||||||
|
regionWidth: number;
|
||||||
|
regionHeight: number;
|
||||||
|
clues: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type Sudoku = {
|
export type Sudoku = {
|
||||||
regionWidth: number;
|
regionWidth: number;
|
||||||
regionHeight: number;
|
regionHeight: number;
|
||||||
@ -17,21 +27,51 @@ export type Sudoku = {
|
|||||||
regions: () => Region[];
|
regions: () => Region[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function generate(
|
const pool = new StaticPool<GenerateArguments, [any, Puzzle]>({
|
||||||
|
size: WORKERS,
|
||||||
|
task: "./src/sudoku/worker.js",
|
||||||
|
});
|
||||||
|
|
||||||
|
const _math = new SudokuMath(0, 0);
|
||||||
|
let activeWorkers = 0;
|
||||||
|
|
||||||
|
export async function generate(
|
||||||
regionWidth: number,
|
regionWidth: number,
|
||||||
regionHeight: number,
|
regionHeight: number,
|
||||||
clues: number
|
clues: number
|
||||||
): Sudoku {
|
): Promise<Sudoku> {
|
||||||
const math = new SudokuMath(regionWidth, regionHeight);
|
if (activeWorkers >= WORKERS) {
|
||||||
const puzzle = math.generatePuzzle(clues);
|
throw new Error("No workers available. Please try again in a moment.");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
try {
|
||||||
regionWidth,
|
activeWorkers++;
|
||||||
regionHeight,
|
const [math, puzzle] = await pool.exec(
|
||||||
cells: math.boardCells,
|
{
|
||||||
raw: puzzle,
|
regionWidth,
|
||||||
rows: () => math.regionsToRows(puzzle, true),
|
regionHeight,
|
||||||
columns: () => math.regionsToCols(puzzle, true),
|
clues,
|
||||||
regions: () => math.chunkRegions(puzzle),
|
},
|
||||||
};
|
TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(_math, math);
|
||||||
|
|
||||||
|
return {
|
||||||
|
regionWidth,
|
||||||
|
regionHeight,
|
||||||
|
cells: math.boardCells,
|
||||||
|
raw: puzzle,
|
||||||
|
rows: () => _math.regionsToRows(puzzle, true),
|
||||||
|
columns: () => _math.regionsToCols(puzzle, true),
|
||||||
|
regions: () => _math.chunkRegions(puzzle),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (isTimeoutError(err)) {
|
||||||
|
throw new Error("Timed out. Try increasing the number of clues.");
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
activeWorkers--;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,7 +227,6 @@ export class SudokuMath {
|
|||||||
}
|
}
|
||||||
if (fails > removeNext.length) {
|
if (fails > removeNext.length) {
|
||||||
fails = 0;
|
fails = 0;
|
||||||
console.log("Backstepping..");
|
|
||||||
Array(removed.length)
|
Array(removed.length)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.forEach(() => replace());
|
.forEach(() => replace());
|
||||||
|
8
src/sudoku/worker.js
Normal file
8
src/sudoku/worker.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const { SudokuMath } = require("./math");
|
||||||
|
const { parentPort } = require("worker_threads");
|
||||||
|
|
||||||
|
parentPort.on("message", ({ regionWidth, regionHeight, clues }) => {
|
||||||
|
const math = new SudokuMath(regionWidth, regionHeight);
|
||||||
|
const puzzle = math.generatePuzzle(clues);
|
||||||
|
parentPort.postMessage([math, puzzle]);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user