Skip to content

Commit fd85412

Browse files
committed
feat: add worker pool control
1 parent 3a9d6f8 commit fd85412

File tree

3 files changed

+145
-73
lines changed

3 files changed

+145
-73
lines changed

demo/index.ts

+61-45
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,89 @@
1-
import { compress,CompressOptions } from "../src";
2-
import {Pane} from 'tweakpane';
1+
import CEngine, { type CompressOptions } from "../src";
2+
import { Pane } from "tweakpane";
33

44
const pane = new Pane();
55

66
const defaultConfig: CompressOptions = {
7-
useWebp: true,
8-
quality: 0.90,
7+
useWebp: false,
8+
quality: 0.9,
99
fileSizeLimit: 30,
10-
lenSizeLimit: 8192
11-
}
10+
lenSizeLimit: 8192,
11+
};
1212

1313
// 添加一个图片上传按钮
1414
const input = document.createElement("input");
1515
input.type = "file";
1616
input.accept = "image/*";
1717
input.multiple = true;
1818

19-
20-
pane.addBinding(defaultConfig, 'useWebp', { label: 'useWebp' });
21-
pane.addBinding(defaultConfig, 'quality', { label: 'quality', min: 0, max: 1 });
22-
pane.addBinding(defaultConfig, 'fileSizeLimit', { label: 'fileSizeLimit', min: 1, max: 100 });
23-
pane.addBinding(defaultConfig, 'lenSizeLimit', { label: 'lenSizeLimit', min: 1000, max: 16383 });
24-
pane.addButton({ title: 'upload for compress' }).on('click', () => {
19+
pane.addBinding(defaultConfig, "useWebp", { label: "useWebp" });
20+
pane.addBinding(defaultConfig, "quality", { label: "quality", min: 0, max: 1 });
21+
pane.addBinding(defaultConfig, "fileSizeLimit", { label: "fileSizeLimit", min: 1, max: 100 });
22+
pane.addBinding(defaultConfig, "lenSizeLimit", { label: "lenSizeLimit", min: 1000, max: 16383 });
23+
pane.addButton({ title: "upload for compress" }).on("click", () => {
2524
input.click();
26-
})
27-
28-
25+
});
2926

3027
const container = document.createElement("div");
3128
container.style.display = "flex";
29+
container.style.flexWrap = "wrap";
3230
document.body.appendChild(container);
3331

3432
input.onchange = async (): Promise<void> => {
3533
try {
36-
console.time("compress");
3734
if (!input.files) return;
38-
const blobs = await compress(input.files, defaultConfig);
39-
console.timeEnd("compress");
40-
// 下载 blob
41-
for (let i = 0; i < blobs.length; i++) {
42-
const blob = blobs[i];
43-
const url = URL.createObjectURL(blob);
44-
const a = document.createElement("a");
45-
a.href = url;
46-
a.download = `compressed-${i + 1}.${blob.type.split("/")[1]}`;
47-
a.textContent = `download`;
48-
const div = document.createElement("div");
49-
const img = document.createElement("img");
50-
img.src = url;
51-
img.style.maxWidth = "100px";
52-
img.style.maxHeight = "100px";
53-
div.appendChild(img);
54-
div.appendChild(a);
55-
container.appendChild(div);
35+
let count = input.files.length;
36+
console.log("files count", count);
37+
console.time("compress");
38+
// const hash = await crypto.digest("SHA-256", await input.files[0].arrayBuffer());
39+
for (let file of input.files) {
40+
const { size: beforeSize, name } = file;
41+
const promise = CEngine.runCompress(file, defaultConfig);
42+
promise.then((blob) => {
43+
const afterSize = blob.size;
44+
console.log(name, "beforeSize:", beforeSize >> 20, "afterSize:", afterSize >> 20, "rate:", (afterSize / beforeSize).toFixed(2));
45+
46+
count--;
47+
if (count === 0) {
48+
console.timeEnd("compress");
49+
}
50+
preDownloadForBlobs(blob);
51+
});
5652
}
53+
54+
// 下载 blob
5755
} catch (error) {
5856
console.error("压缩失败", error);
5957
}
6058
};
6159

6260
// 页面流畅性检测
63-
let time = new Date().getTime();
64-
const show = () => {
65-
const cur = new Date().getTime();
66-
const timeDiff = cur - time;
67-
if (timeDiff > 1000) {
68-
console.log("time:", timeDiff);
69-
}
70-
time = cur;
71-
requestAnimationFrame(show);
72-
};
73-
show();
61+
// let time = new Date().getTime();
62+
// const show = () => {
63+
// const cur = new Date().getTime();
64+
// const timeDiff = cur - time;
65+
// if (timeDiff > 1000) {
66+
// console.log("time:", timeDiff);
67+
// }
68+
// time = cur;
69+
// requestAnimationFrame(show);
70+
// };
71+
// show();
72+
73+
function preDownloadForBlobs(blob: Blob, i = 0): void {
74+
const url = URL.createObjectURL(blob);
75+
const a = document.createElement("a");
76+
a.href = url;
77+
a.download = `compressed-${i + 1}.${blob.type.split("/")[1]}`;
78+
a.textContent = `download`;
79+
80+
const div = document.createElement("div");
81+
const img = document.createElement("img");
82+
img.src = url;
83+
img.style.maxWidth = "100px";
84+
img.style.maxHeight = "100px";
85+
86+
div.appendChild(img);
87+
div.appendChild(a);
88+
container.appendChild(div);
89+
}

src/compress.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface CompressOptions {
3636
}
3737

3838
export async function compressImg(file: File, options?: CompressOptions): Promise<Blob> {
39-
// const { type: originType, size: originSize } = file;
39+
const { type: originType, size: originSize } = file;
4040
const bitmap = await createImageBitmap(file);
4141
let { width, height } = bitmap;
4242
let { quality = 0.9, lenSizeLimit = 8192, fileSizeLimit = 30, useWebp = true } = options || {};
@@ -63,8 +63,9 @@ export async function compressImg(file: File, options?: CompressOptions): Promis
6363

6464
/**max len 最大尺寸 */
6565
const mLen = Math.min(lenSizeLimit, MaxLen);
66-
let scale = 1;
67-
if (area > MaxArea || width > lenSizeLimit || height > lenSizeLimit) {
66+
// jpg 直接缩放尺寸
67+
let scale = originType === "image/jpeg" ? Math.sqrt(mSize / originSize) : 1;
68+
if (area * scale * scale > MaxArea || width * scale > mLen || height * scale > mLen) {
6869
scale = Math.min(Math.sqrt(MaxArea / area), mLen / width, mLen / height);
6970
}
7071

src/index.ts

+80-25
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,91 @@
1-
import { type CompressOptions } from './compress';
2-
import MyWorker from './worker?worker&inline';
3-
import type { MyWorkerType } from './worker';
4-
5-
export async function compress(files: FileList | File[], options?: CompressOptions): Promise<Blob[]> {
6-
const promises: Promise<Blob>[] = new Array();
7-
for (const file of files) {
8-
const promise = new Promise<Blob>(async (resolve, reject) => {
9-
if (!file.type.startsWith('image/')) {
10-
throw new Error('only image support');
11-
}
12-
const { fileSizeLimit: FileSizeLimit = 30 } = options || {};
13-
// 小于 30Mb 的图片不压缩
14-
if (file.size <= FileSizeLimit << 20) {
15-
resolve(file);
1+
import { type CompressOptions } from "./compress";
2+
import MyWorker from "./worker?worker&inline";
3+
4+
class WorkerPool {
5+
size: number;
6+
workers?: Worker[];
7+
queue: { file: File; options?: CompressOptions, resolve: (value: Blob | PromiseLike<Blob>) => void }[];
8+
timer?: number;
9+
10+
constructor(size: number) {
11+
this.size = size;
12+
this.queue = [];
13+
}
14+
15+
init() {
16+
this.workers = new Array(this.size);
17+
for (let i = 0; i < this.size; i++) {
18+
this.workers[i] = new MyWorker();
19+
}
20+
}
21+
22+
resetTimer() {
23+
if (this.timer) {
24+
clearTimeout(this.timer);
25+
}
26+
this.timer = setTimeout(() => {
27+
this.terminateAllWorkers();
28+
}, 60000); // 1 minute
29+
}
30+
31+
terminateAllWorkers() {
32+
this.workers?.forEach((worker) => {
33+
worker.terminate();
34+
console.log("terminate worker -->");
35+
})
36+
this.workers = undefined;
37+
this.timer = undefined;
38+
}
39+
40+
async runCompress(file: File, options?: CompressOptions) {
41+
this.resetTimer();
42+
if (this.workers === undefined) {
43+
this.init();
44+
}
45+
const workers = this.workers!;
46+
if (!file.type.startsWith("image/")) {
47+
throw new Error("only image support");
48+
}
49+
const { fileSizeLimit: FileSizeLimit = 30 } = options || {};
50+
// 小于 30Mb 的图片不压缩
51+
if (file.size <= FileSizeLimit << 20) {
52+
return file;
53+
}
54+
const promise = new Promise<Blob>((resolve, reject) => {
55+
// 线程不够用,将任务放入队列
56+
if (workers.length <= 0) {
57+
this.queue.push({ file, options, resolve });
1658
return;
17-
}
18-
const worker: MyWorkerType = new MyWorker;
19-
worker.onmessage = (event) => {
20-
resolve(event.data);
21-
worker.terminate();
59+
}
60+
// 有线程可用
61+
const worker = workers.pop()!;
62+
worker.onmessage = (e) => {
63+
// 线程执行完毕,将线程放回线程池
64+
resolve(e.data);
65+
workers.push(worker);
66+
// 如果队列中还有任务,继续执行
67+
if (this.queue.length > 0) {
68+
const { file, options, resolve } = this.queue.shift()!;
69+
resolve(this.runCompress(file, options));
70+
}
2271
};
2372
worker.onerror = (error) => {
2473
reject(error);
2574
worker.terminate();
2675
};
27-
28-
// file 不是 Transferable 对象,传递成本较高,但如果使用 transferable arraybuffer, 因为增加了 file -> ArrayBuffer -> file 的转换,时间成本更高,好处是减少了 gc
2976
worker.postMessage([file, options]);
3077
});
31-
promises.push(promise);
78+
return promise;
3279
}
33-
return Promise.all(promises);
3480
}
3581

36-
export type { CompressOptions };
82+
// 获取硬件并发性(CPU 核心数)
83+
const hardwareConcurrency = navigator.hardwareConcurrency;
84+
const count = Math.min(hardwareConcurrency, 10);
85+
86+
/**CompressEngine */
87+
const CEngine = new WorkerPool(count);
88+
89+
export default CEngine;
90+
91+
export type { CompressOptions };

0 commit comments

Comments
 (0)