Skip to content

Commit 6978493

Browse files
authored
Merge pull request #4 from thinknathan/thread-fix
Thread fix
2 parents 63deffe + 901f88e commit 6978493

File tree

9 files changed

+197
-70
lines changed

9 files changed

+197
-70
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-image-slice",
3-
"version": "2.2.1",
3+
"version": "2.2.2",
44
"description": "Slices an input image into segments according to specified width and height",
55
"repository": {
66
"type": "git",

slice.cjs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ const yargs = require('yargs');
55
const os = require('os');
66
const processImage_1 = require('./utils/processImage');
77
const processPath_1 = require('./utils/processPath');
8-
function main() {
8+
async function main() {
9+
// console.time('Done in');
910
// Parse command line arguments
10-
const options = yargs
11+
const options = await yargs(process.argv.slice(2))
1112
.option('f', {
1213
alias: 'filename',
1314
describe: 'Input image filename',
@@ -94,7 +95,8 @@ function main() {
9495
'Uses bicubic interpolation instead of nearest neighbour if rescaling',
9596
type: 'boolean',
9697
default: false,
97-
}).argv;
98+
})
99+
.parse();
98100
if (options.filename) {
99101
// Process a single image
100102
(0, processImage_1.sliceImage)(options);
@@ -107,12 +109,13 @@ function main() {
107109
console.error(err);
108110
}
109111
numCores = Math.max(numCores - 1, 1); // Min 1
110-
numCores = Math.min(numCores, 16); // Max 16
111-
(0, processPath_1.processPath)(options.folderPath, options, numCores);
112+
numCores = Math.min(numCores, 32); // Max 32
113+
await (0, processPath_1.processPath)(options.folderPath, options, numCores);
112114
} else {
113115
console.error(
114116
'Error: Requires either `filename` or `folderPath`. Run `slice --help` for help.',
115117
);
116118
}
119+
// console.timeEnd('Done in');
117120
}
118121
main();

src/slice.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import * as os from 'os';
66
import { sliceImage } from './utils/processImage';
77
import { processPath } from './utils/processPath';
88

9-
function main() {
9+
async function main() {
10+
// console.time('Done in');
11+
1012
// Parse command line arguments
11-
const options = yargs
13+
const options = (await yargs(process.argv.slice(2))
1214
.option('f', {
1315
alias: 'filename',
1416
describe: 'Input image filename',
@@ -95,7 +97,8 @@ function main() {
9597
'Uses bicubic interpolation instead of nearest neighbour if rescaling',
9698
type: 'boolean',
9799
default: false,
98-
}).argv as unknown as Options;
100+
})
101+
.parse()) as unknown as Options;
99102

100103
if (options.filename) {
101104
// Process a single image
@@ -109,13 +112,14 @@ function main() {
109112
console.error(err);
110113
}
111114
numCores = Math.max(numCores - 1, 1); // Min 1
112-
numCores = Math.min(numCores, 16); // Max 16
113-
processPath(options.folderPath, options, numCores);
115+
numCores = Math.min(numCores, 32); // Max 32
116+
await processPath(options.folderPath, options, numCores);
114117
} else {
115118
console.error(
116119
'Error: Requires either `filename` or `folderPath`. Run `slice --help` for help.',
117120
);
118121
}
122+
// console.timeEnd('Done in');
119123
}
120124

121125
main();

src/utils/processImage.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import * as Jimp from 'jimp';
22
import * as fs from 'fs';
33
import * as path from 'path';
4-
import { workerData, isMainThread } from 'worker_threads';
4+
import { parentPort, isMainThread } from 'worker_threads';
55

66
function errorCallback(err: unknown) {
77
if (err) {
88
console.error(err);
99
}
1010
}
1111

12+
/**
13+
* Called on a worker thread to signal current work is complete
14+
*/
15+
const workerIsDone = () => parentPort?.postMessage('complete');
16+
1217
/**
1318
* Function to slice an image into smaller segments
1419
*/
1520
export function sliceImage(options: Options, skipExtCheck?: boolean): void {
16-
console.time('Done in');
1721
const { filename } = options;
1822
Jimp.read(filename!)
1923
.then((image) => {
@@ -76,6 +80,8 @@ function continueSlicing(image: Jimp, options: Options): void {
7680
// Calculate the number of slices in both dimensions
7781
const horizontalSlices = Math.ceil(imageWidth / width);
7882
const verticalSlices = Math.ceil(imageHeight / height);
83+
const totalSlices = horizontalSlices * verticalSlices;
84+
let savedSlices = 0;
7985

8086
// Create a folder for output if it doesn't exist
8187
const outputFolder = 'output';
@@ -98,6 +104,14 @@ function continueSlicing(image: Jimp, options: Options): void {
98104
const baseFilename = path.basename(filename!, path.extname(filename!));
99105
const outputFilename = `${outputFolder}/${baseFilename}_${x}_${y}.png`;
100106

107+
const finishedSavingFile = () => {
108+
console.log(`Slice saved: ${outputFilename}`);
109+
savedSlices++;
110+
if (savedSlices === totalSlices && !isMainThread) {
111+
workerIsDone();
112+
}
113+
};
114+
101115
if (canvasWidth || canvasHeight) {
102116
// Calculate canvas dimensions
103117
const finalCanvasWidth = canvasWidth || width;
@@ -120,27 +134,34 @@ function continueSlicing(image: Jimp, options: Options): void {
120134
cubic ? Jimp.RESIZE_BICUBIC : Jimp.RESIZE_NEAREST_NEIGHBOR,
121135
);
122136
}
123-
canvas.write(outputFilename, errorCallback);
137+
canvas
138+
.writeAsync(outputFilename)
139+
.then(finishedSavingFile)
140+
.catch(errorCallback);
124141
} else {
125142
if (scale !== 1) {
126143
slice.scale(
127144
scale,
128145
cubic ? Jimp.RESIZE_BICUBIC : Jimp.RESIZE_NEAREST_NEIGHBOR,
129146
);
130147
}
131-
slice.write(outputFilename, errorCallback);
148+
slice
149+
.writeAsync(outputFilename)
150+
.then(finishedSavingFile)
151+
.catch(errorCallback);
132152
}
133-
134-
console.log(`Slice saved: ${outputFilename}`);
135153
}
136154
}
137-
console.timeEnd('Done in');
138155
}
139156

140157
// If used as a worker thread, get file name from message
141158
if (!isMainThread) {
142-
const { filePath, options } = workerData;
143-
options.filename = filePath;
144-
145-
sliceImage(options, true);
159+
parentPort?.on(
160+
'message',
161+
async (message: { filePath: string; options: Options }) => {
162+
const { filePath, options } = message;
163+
options.filename = filePath;
164+
sliceImage(options, true);
165+
},
166+
);
146167
}

src/utils/processPath.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export async function processPath(
99
directoryPath: string,
1010
options: Options,
1111
maxWorkers: number,
12-
): Promise<void> {
12+
): Promise<boolean> {
1313
const workerPool = new WorkerPool(maxWorkers);
1414

1515
try {
@@ -24,10 +24,13 @@ export async function processPath(
2424
workerPool.addTask(filePath, options);
2525
}
2626
}
27-
28-
// Wait for all tasks to complete before exiting
29-
workerPool.waitForCompletion();
3027
} catch (err) {
3128
console.error(`Error reading directory: ${directoryPath}`, err);
3229
}
30+
31+
await workerPool.allComplete();
32+
33+
workerPool.exitAll();
34+
35+
return true;
3336
}

src/utils/workerPool.ts

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,48 @@
11
import { Worker } from 'worker_threads';
22
import * as path from 'path';
33

4+
type TWorker = Worker & { isIdle: boolean };
5+
46
/**
57
* Manages a pool of worker threads for parallel processing of image files.
68
*/
79
export class WorkerPool {
8-
private workers: Worker[] = [];
10+
private workers: TWorker[] = [];
911
private taskQueue: { filePath: string; options: Options }[] = [];
1012
private maxWorkers: number;
13+
private completePromise?: Promise<void>;
14+
private completeResolve?: () => void;
15+
private isComplete(): boolean {
16+
return (
17+
this.taskQueue.length === 0 &&
18+
this.workers.every((worker) => worker.isIdle)
19+
);
20+
}
21+
22+
/**
23+
* Terminate all workers in the pool.
24+
*/
25+
public exitAll(): void {
26+
this.workers.forEach((worker) => worker.terminate());
27+
this.workers = [];
28+
}
29+
30+
/**
31+
* Returns a promise that resolves when all work is done.
32+
*/
33+
public async allComplete(): Promise<void> {
34+
if (this.isComplete()) {
35+
return Promise.resolve();
36+
}
37+
38+
if (!this.completePromise) {
39+
this.completePromise = new Promise<void>((resolve) => {
40+
this.completeResolve = resolve;
41+
});
42+
}
43+
44+
return this.completePromise;
45+
}
1146

1247
/**
1348
* Creates a new WorkerPool instance.
@@ -25,18 +60,22 @@ export class WorkerPool {
2560
* @param options - Image processing options for the file.
2661
*/
2762
private createWorker(filePath: string, options: Options): void {
28-
const worker = new Worker(path.join(__dirname, 'processImage.js'), {
29-
workerData: { filePath, options },
30-
});
63+
const worker = new Worker(
64+
path.join(__dirname, 'processImage.js'),
65+
) as TWorker;
66+
67+
worker.isIdle = false;
68+
worker.postMessage({ filePath, options });
3169

3270
// Listen for messages and errors from the worker
33-
worker.on('message', (message) => {
34-
console.log(message);
71+
worker.on('message', () => {
72+
worker.isIdle = true;
3573
this.processNextTask();
3674
});
3775

3876
worker.on('error', (err) => {
3977
console.error(`Error in worker for file ${filePath}:`, err);
78+
worker.isIdle = true;
4079
this.processNextTask();
4180
});
4281

@@ -49,7 +88,22 @@ export class WorkerPool {
4988
private processNextTask(): void {
5089
const nextTask = this.taskQueue.shift();
5190
if (nextTask) {
52-
this.createWorker(nextTask.filePath, nextTask.options);
91+
if (this.workers.length < this.maxWorkers) {
92+
this.createWorker(nextTask.filePath, nextTask.options);
93+
} else {
94+
const worker = this.workers.find((w) => w.isIdle);
95+
if (worker) {
96+
worker.isIdle = false;
97+
worker.postMessage(nextTask);
98+
} else {
99+
// Something went wrong, there are no idle workers somehow
100+
throw Error('Could not find an idle worker.');
101+
}
102+
}
103+
} else if (this.isComplete() && this.completeResolve) {
104+
this.completeResolve();
105+
this.completePromise = undefined;
106+
this.completeResolve = undefined;
53107
}
54108
}
55109

@@ -66,15 +120,4 @@ export class WorkerPool {
66120
this.taskQueue.push({ filePath, options });
67121
}
68122
}
69-
70-
/**
71-
* Waits for all tasks to complete before exiting.
72-
*/
73-
public waitForCompletion(): void {
74-
this.workers.forEach((worker) => {
75-
worker.on('exit', () => {
76-
this.processNextTask();
77-
});
78-
});
79-
}
80123
}

0 commit comments

Comments
 (0)