Skip to content

Commit f082507

Browse files
committed
feat: add bounded LZMA2 multithreading
1 parent 15b2df5 commit f082507

10 files changed

Lines changed: 446 additions & 48 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,16 @@ bun run test:bun # run the same tests under the Bun runtime
3131

3232
## Usage
3333

34-
### diff(originBuf, newBuf)
34+
### diff(originBuf, newBuf[, options])
3535

3636
Compare two buffers and return a new hdiffpatch patch as return value.
3737

38+
All diff entry points accept an optional `options` object with
39+
`compressionThreads: 1 | 2`. Two threads enable LZMA's internal parallel
40+
match finder while preserving compression level 9 and the 8 MiB dictionary.
41+
The default remains one thread. `diffWindow()` also accepts `windowSize` in
42+
the options object; the legacy positional `windowSize` remains supported.
43+
3844
### diffSingleStream(oldPath, newPath, outDiffPath[, cb])
3945

4046
Create a **single-format** (same wire format as `diff()`) patch by streaming
@@ -67,6 +73,7 @@ additional memory.
6773
`capabilities.diffWindowVerifiesOutput` are `true`. Each native diff function
6874
applies and compares the generated patch before it returns, so orchestration
6975
layers can avoid running a redundant second round-trip check.
76+
`capabilities.maxCompressionThreads` is `2`.
7077

7178
### patchSingleStream(oldPath, diffPath, outNewPath[, cb])
7279

binding.gyp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@
2424
"HDiffPatch/libHDiffPatch/HDiff/private_diff/match_inplace.cpp",
2525
"lzma/C/CpuArch.c",
2626
"lzma/C/LzFind.c",
27+
"lzma/C/LzFindMt.c",
28+
"lzma/C/LzFindOpt.c",
2729
"lzma/C/LzmaDec.c",
2830
"lzma/C/LzmaEnc.c",
2931
"lzma/C/Lzma2Dec.c",
30-
"lzma/C/Lzma2Enc.c"
32+
"lzma/C/Lzma2Enc.c",
33+
"lzma/C/MtCoder.c",
34+
"lzma/C/Threads.c"
3135
],
3236
"defines": [
3337
"_IS_NEED_DIR_DIFF_PATCH=0",
34-
"_7ZIP_ST",
35-
"Z7_ST",
3638
"_IS_USED_MULTITHREAD=0",
3739
"_IS_OUT_DIFF_INFO=0",
3840
"NAPI_VERSION=8"

index.d.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,48 @@ export type BinaryLike = Buffer | ArrayBufferView;
55
export type DiffCallback = (err: Error | null, result?: Buffer) => void;
66
export type StreamCallback = (err: Error | null, outPath?: string) => void;
77

8+
export interface CompressionOptions {
9+
/** LZMA2 compression workers. Level 9 and the 8 MiB dictionary are unchanged. */
10+
compressionThreads?: 1 | 2;
11+
}
12+
13+
export interface DiffWindowOptions extends CompressionOptions {
14+
/** Old-data sliding window bytes; 0 uses the native 2 MiB default. */
15+
windowSize?: number;
16+
}
17+
818
export interface NativeAddon {
919
diff(oldBuf: BinaryLike, newBuf: BinaryLike): Buffer;
20+
diff(oldBuf: BinaryLike, newBuf: BinaryLike, options: CompressionOptions): Buffer;
1021
diff(oldBuf: BinaryLike, newBuf: BinaryLike, cb: DiffCallback): void;
22+
diff(
23+
oldBuf: BinaryLike,
24+
newBuf: BinaryLike,
25+
options: CompressionOptions,
26+
cb: DiffCallback
27+
): void;
1128
patch(oldBuf: BinaryLike, diffBuf: BinaryLike): Buffer;
1229
patch(oldBuf: BinaryLike, diffBuf: BinaryLike, cb: DiffCallback): void;
1330
diffStream(oldPath: string, newPath: string, outDiffPath: string): string;
31+
diffStream(
32+
oldPath: string,
33+
newPath: string,
34+
outDiffPath: string,
35+
options: CompressionOptions
36+
): string;
1437
diffStream(
1538
oldPath: string,
1639
newPath: string,
1740
outDiffPath: string,
1841
cb: StreamCallback
1942
): void;
43+
diffStream(
44+
oldPath: string,
45+
newPath: string,
46+
outDiffPath: string,
47+
options: CompressionOptions,
48+
cb: StreamCallback
49+
): void;
2050
patchStream(oldPath: string, diffPath: string, outNewPath: string): string;
2151
patchStream(
2252
oldPath: string,
@@ -29,6 +59,19 @@ export interface NativeAddon {
2959
oldPath: string,
3060
newPath: string,
3161
outDiffPath: string,
62+
options: CompressionOptions
63+
): string;
64+
diffSingleStream(
65+
oldPath: string,
66+
newPath: string,
67+
outDiffPath: string,
68+
cb: StreamCallback,
69+
): void;
70+
diffSingleStream(
71+
oldPath: string,
72+
newPath: string,
73+
outDiffPath: string,
74+
options: CompressionOptions,
3275
cb: StreamCallback,
3376
): void;
3477
patchSingleStream(oldPath: string, diffPath: string, outNewPath: string): string;
@@ -48,6 +91,19 @@ export interface NativeAddon {
4891
oldPath: string,
4992
newPath: string,
5093
outDiffPath: string,
94+
options: DiffWindowOptions
95+
): string;
96+
diffWindow(
97+
oldPath: string,
98+
newPath: string,
99+
outDiffPath: string,
100+
cb: StreamCallback
101+
): void;
102+
diffWindow(
103+
oldPath: string,
104+
newPath: string,
105+
outDiffPath: string,
106+
options: DiffWindowOptions,
51107
cb: StreamCallback
52108
): void;
53109
diffWindow(
@@ -65,6 +121,7 @@ export interface HdiffpatchCapabilities {
65121
readonly diffStreamVerifiesOutput: true;
66122
readonly diffSingleStreamVerifiesOutput: true;
67123
readonly diffWindowVerifiesOutput: true;
124+
readonly maxCompressionThreads: 2;
68125
}
69126

70127
/** Native diff functions apply and compare their output before returning. */
@@ -74,6 +131,17 @@ export function diff(oldBuf: BinaryLike, newBuf: BinaryLike): Buffer;
74131
export function diff(
75132
oldBuf: BinaryLike,
76133
newBuf: BinaryLike,
134+
options: CompressionOptions
135+
): Buffer;
136+
export function diff(
137+
oldBuf: BinaryLike,
138+
newBuf: BinaryLike,
139+
cb: DiffCallback
140+
): void;
141+
export function diff(
142+
oldBuf: BinaryLike,
143+
newBuf: BinaryLike,
144+
options: CompressionOptions,
77145
cb: DiffCallback
78146
): void;
79147

@@ -93,6 +161,19 @@ export function diffStream(
93161
oldPath: string,
94162
newPath: string,
95163
outDiffPath: string,
164+
options: CompressionOptions
165+
): string;
166+
export function diffStream(
167+
oldPath: string,
168+
newPath: string,
169+
outDiffPath: string,
170+
cb: StreamCallback
171+
): void;
172+
export function diffStream(
173+
oldPath: string,
174+
newPath: string,
175+
outDiffPath: string,
176+
options: CompressionOptions,
96177
cb: StreamCallback
97178
): void;
98179

@@ -116,6 +197,19 @@ export function diffSingleStream(
116197
oldPath: string,
117198
newPath: string,
118199
outDiffPath: string,
200+
options: CompressionOptions,
201+
): string;
202+
export function diffSingleStream(
203+
oldPath: string,
204+
newPath: string,
205+
outDiffPath: string,
206+
cb: StreamCallback,
207+
): void;
208+
export function diffSingleStream(
209+
oldPath: string,
210+
newPath: string,
211+
outDiffPath: string,
212+
options: CompressionOptions,
119213
cb: StreamCallback,
120214
): void;
121215
export function patchSingleStream(
@@ -144,6 +238,19 @@ export function diffWindow(
144238
oldPath: string,
145239
newPath: string,
146240
outDiffPath: string,
241+
options: DiffWindowOptions
242+
): string;
243+
export function diffWindow(
244+
oldPath: string,
245+
newPath: string,
246+
outDiffPath: string,
247+
cb: StreamCallback
248+
): void;
249+
export function diffWindow(
250+
oldPath: string,
251+
newPath: string,
252+
outDiffPath: string,
253+
options: DiffWindowOptions,
147254
cb: StreamCallback
148255
): void;
149256
export function diffWindow(

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ exports.capabilities = Object.freeze({
4646
diffStreamVerifiesOutput: true,
4747
diffSingleStreamVerifiesOutput: true,
4848
diffWindowVerifiesOutput: true,
49+
maxCompressionThreads: 2,
4950
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"test:bun": "bun ./test/test.js",
1919
"prepublishOnly": "bun scripts/prepublish.ts",
2020
"benchmark": "node --expose-gc test/benchmark.js",
21+
"benchmark:threads": "node test/benchmark-threads.js",
2122
"prebuild": "prebuildify --napi --strip"
2223
},
2324
"gypfile": true,

src/hdiff.cpp

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,28 @@
1010
#define IS_NOTICE_compress_canceled 0
1111
#include "../lzma/C/Lzma2Dec.h"
1212
#include "../lzma/C/Lzma2Enc.h"
13-
#include "../lzma/C/MtCoder.h"
13+
// The public API deliberately caps LZMA's internal match-finder parallelism
14+
// at two workers, so the C++ wrapper only needs the compressor's bound here.
15+
// MtCoder itself is compiled as C from binding.gyp.
16+
#define MTCODER_THREADS_MAX 2
1417
#include "../HDiffPatch/compress_plugin_demo.h"
1518
#include "../HDiffPatch/decompress_plugin_demo.h"
1619

1720
namespace {
1821
// 与 v1.0.6 起的历史产物保持一致的压缩参数(patch 兼容性由 single
1922
// 格式规范保证,这里的一致性只为产物尺寸/确定性稳定)
20-
void configure_lzma2(TCompressPlugin_lzma2& compressPlugin) {
23+
void configure_lzma2(TCompressPlugin_lzma2& compressPlugin,
24+
size_t compressionThreads) {
25+
if (compressionThreads < 1 || compressionThreads > 2) {
26+
throw std::runtime_error("compressionThreads must be 1 or 2.");
27+
}
2128
const size_t myBestDictSize = (1 << 20) * 8; // 固定 8MB
2229
compressPlugin = lzma2CompressPlugin;
2330
compressPlugin.compress_level = 9;
2431
compressPlugin.dict_size = myBestDictSize;
25-
compressPlugin.thread_num = 1;
32+
// 2 线程只启用 LZMA 内部并行匹配,不切 LZMA2 block,避免额外的
33+
// block 级内存放大。压缩级别和字典保持历史值不变。
34+
compressPlugin.thread_num = static_cast<int>(compressionThreads);
2635
}
2736

2837
// 升级到 HDiffPatch v5 前的历史参数:匹配分 3、步进内存 256KB。
@@ -255,10 +264,10 @@ namespace {
255264
}
256265

257266
void hdiff(const uint8_t* old, size_t oldsize, const uint8_t* _new, size_t newsize,
258-
std::vector<uint8_t>& out_codeBuf) {
267+
std::vector<uint8_t>& out_codeBuf, size_t compressionThreads) {
259268
hpatch_TDecompress* decompressPlugin = &lzma2DecompressPlugin;
260269
TCompressPlugin_lzma2 compressPlugin;
261-
configure_lzma2(compressPlugin);
270+
configure_lzma2(compressPlugin, compressionThreads);
262271

263272
create_single_compressed_diff(_new, _new + newsize, old, old + oldsize, out_codeBuf,
264273
&compressPlugin.base, kPatchStepMemSize,
@@ -273,14 +282,15 @@ void hdiff(const uint8_t* old, size_t oldsize, const uint8_t* _new, size_t newsi
273282
}
274283
}
275284

276-
void hdiff_stream(const char* oldPath,const char* newPath,const char* outDiffPath){
285+
void hdiff_stream(const char* oldPath,const char* newPath,const char* outDiffPath,
286+
size_t compressionThreads){
277287
if (!oldPath || !newPath || !outDiffPath) {
278288
throw std::runtime_error("Invalid file path.");
279289
}
280290

281291
hpatch_TDecompress* decompressPlugin = &lzma2DecompressPlugin;
282292
TCompressPlugin_lzma2 compressPlugin;
283-
configure_lzma2(compressPlugin);
293+
configure_lzma2(compressPlugin, compressionThreads);
284294

285295
FileStreamGuard streams;
286296
streams.openInputs(oldPath, newPath);
@@ -300,14 +310,14 @@ void hdiff_stream(const char* oldPath,const char* newPath,const char* outDiffPat
300310
}
301311

302312
void hdiff_window(const char* oldPath,const char* newPath,const char* outDiffPath,
303-
size_t windowSize){
313+
size_t windowSize,size_t compressionThreads){
304314
if (!oldPath || !newPath || !outDiffPath) {
305315
throw std::runtime_error("Invalid file path.");
306316
}
307317

308318
hpatch_TDecompress* decompressPlugin = &lzma2DecompressPlugin;
309319
TCompressPlugin_lzma2 compressPlugin;
310-
configure_lzma2(compressPlugin);
320+
configure_lzma2(compressPlugin, compressionThreads);
311321

312322
FileStreamGuard streams;
313323
streams.openInputs(oldPath, newPath);
@@ -336,14 +346,15 @@ void hdiff_window(const char* oldPath,const char* newPath,const char* outDiffPat
336346
streams.closeAllOrThrow();
337347
}
338348

339-
void hdiff_single_stream(const char* oldPath,const char* newPath,const char* outDiffPath){
349+
void hdiff_single_stream(const char* oldPath,const char* newPath,const char* outDiffPath,
350+
size_t compressionThreads){
340351
if (!oldPath || !newPath || !outDiffPath) {
341352
throw std::runtime_error("Invalid file path.");
342353
}
343354

344355
hpatch_TDecompress* decompressPlugin = &lzma2DecompressPlugin;
345356
TCompressPlugin_lzma2 compressPlugin;
346-
configure_lzma2(compressPlugin);
357+
configure_lzma2(compressPlugin, compressionThreads);
347358

348359
FileStreamGuard streams;
349360
streams.openInputs(oldPath, newPath);

src/hdiff.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@
99
#include <vector>
1010

1111
void hdiff(const uint8_t* old,size_t oldsize,const uint8_t* _new,size_t newsize,
12-
std::vector<uint8_t>& out_codeBuf);
12+
std::vector<uint8_t>& out_codeBuf,size_t compressionThreads=1);
1313
// HDIFF13 流式(生成端低内存,产物需 patchStream 应用)
14-
void hdiff_stream(const char* oldPath,const char* newPath,const char* outDiffPath);
14+
void hdiff_stream(const char* oldPath,const char* newPath,const char* outDiffPath,
15+
size_t compressionThreads=1);
1516
// HDIFFSF20 single 格式的流式生成(生成端低内存,产物与 diff() 同格式,
1617
// 任何既有 single 应用端可直接使用)
17-
void hdiff_single_stream(const char* oldPath,const char* newPath,const char* outDiffPath);
18+
void hdiff_single_stream(const char* oldPath,const char* newPath,const char* outDiffPath,
19+
size_t compressionThreads=1);
1820
// HDIFFSF20 single 格式的 window 模式生成:大块流式匹配 + 窗口内后缀串
1921
// 精修,匹配质量接近内存版而内存占用保持流式档;产物与 diff() 同格式。
2022
// windowSize 为 old 数据滑动窗口字节数,0 表示用默认值(2MB);窗口越大
2123
// 能捕获越长距离的内容移动,内存占用近似随之线性增长。
2224
void hdiff_window(const char* oldPath,const char* newPath,const char* outDiffPath,
23-
size_t windowSize=0);
25+
size_t windowSize=0,size_t compressionThreads=1);
2426

2527
#endif

0 commit comments

Comments
 (0)