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:
Matt Low 2021-02-20 18:57:18 -07:00
parent 7f9cffbfdd
commit 7ac3e6ad06
7 changed files with 84 additions and 22 deletions

20
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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: (

View File

@ -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--;
}
} }

View File

@ -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
View 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]);
});

View File