vibe_zstd 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +79 -3
- data/ext/vibe_zstd/cctx.c +71 -25
- data/ext/vibe_zstd/dctx.c +260 -32
- data/ext/vibe_zstd/depend +3 -0
- data/ext/vibe_zstd/dict.c +51 -19
- data/ext/vibe_zstd/extconf.rb +7 -4
- data/ext/vibe_zstd/frames.c +13 -2
- data/ext/vibe_zstd/streaming.c +110 -16
- data/ext/vibe_zstd/vibe_zstd.c +30 -0
- data/ext/vibe_zstd/vibe_zstd.h +1 -0
- data/lib/vibe_zstd/version.rb +1 -1
- data/lib/vibe_zstd.rb +48 -23
- metadata +3 -2
data/ext/vibe_zstd/streaming.c
CHANGED
|
@@ -14,6 +14,12 @@ static VALUE vibe_zstd_reader_initialize(int argc, VALUE *argv, VALUE self);
|
|
|
14
14
|
static VALUE vibe_zstd_reader_read(int argc, VALUE *argv, VALUE self);
|
|
15
15
|
static VALUE vibe_zstd_reader_eof(VALUE self);
|
|
16
16
|
|
|
17
|
+
// State struct for rb_ensure-based string lock/unlock in vibe_zstd_writer_write
|
|
18
|
+
typedef struct {
|
|
19
|
+
vibe_zstd_cstream* cstream;
|
|
20
|
+
VALUE data;
|
|
21
|
+
} vibe_zstd_write_state;
|
|
22
|
+
|
|
17
23
|
// TypedData types - defined in vibe_zstd.c
|
|
18
24
|
extern rb_data_type_t vibe_zstd_cstream_type;
|
|
19
25
|
extern rb_data_type_t vibe_zstd_dstream_type;
|
|
@@ -89,6 +95,9 @@ vibe_zstd_writer_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
89
95
|
if (ZSTD_isError(result)) {
|
|
90
96
|
rb_raise(rb_eRuntimeError, "Failed to set dictionary: %s", ZSTD_getErrorName(result));
|
|
91
97
|
}
|
|
98
|
+
// Retain the CDict object so GC won't free it while the stream holds a raw
|
|
99
|
+
// pointer to its internal ZSTD_CDict (ZSTD_CCtx_refCDict stores no Ruby ref)
|
|
100
|
+
rb_ivar_set(self, rb_intern("@dict"), dict);
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
// Allocate reusable output buffer (write barrier for WB_PROTECTED)
|
|
@@ -97,14 +106,16 @@ vibe_zstd_writer_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
97
106
|
return self;
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
// Body of the rb_ensure wrapper: runs the compress loop with data locked
|
|
100
110
|
static VALUE
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
111
|
+
vibe_zstd_writer_write_body(VALUE arg) {
|
|
112
|
+
vibe_zstd_write_state* state = (vibe_zstd_write_state*)arg;
|
|
113
|
+
vibe_zstd_cstream* cstream = state->cstream;
|
|
114
|
+
VALUE data = state->data;
|
|
115
|
+
|
|
116
|
+
// Input buffer: pos advances as ZSTD consumes data.
|
|
117
|
+
// data is locked (rb_str_locktmp) for the duration of this call so that
|
|
118
|
+
// RSTRING_PTR remains valid even when rb_funcall runs arbitrary Ruby code.
|
|
108
119
|
ZSTD_inBuffer input = {
|
|
109
120
|
.src = RSTRING_PTR(data),
|
|
110
121
|
.size = RSTRING_LEN(data),
|
|
@@ -116,7 +127,11 @@ vibe_zstd_writer_write(VALUE self, VALUE data) {
|
|
|
116
127
|
|
|
117
128
|
// Process all input data in chunks
|
|
118
129
|
while (input.pos < input.size) {
|
|
119
|
-
|
|
130
|
+
// Unshare buffer if COW-shared by a prior IO#write receiver (Ruby 3.3+),
|
|
131
|
+
// then restore capacity which may have shrunk during unsharing
|
|
132
|
+
rb_str_modify(outBuffer);
|
|
133
|
+
rb_str_resize(outBuffer, (long)outBufferSize);
|
|
134
|
+
rb_str_set_len(outBuffer, 0);
|
|
120
135
|
ZSTD_outBuffer output = {
|
|
121
136
|
.dst = RSTRING_PTR(outBuffer),
|
|
122
137
|
.size = outBufferSize,
|
|
@@ -133,10 +148,39 @@ vibe_zstd_writer_write(VALUE self, VALUE data) {
|
|
|
133
148
|
// Write any compressed output that was produced
|
|
134
149
|
if (output.pos > 0) {
|
|
135
150
|
rb_str_set_len(outBuffer, output.pos);
|
|
151
|
+
// rb_funcall may run arbitrary Ruby code, but input.src stays valid
|
|
152
|
+
// because data is locked against mutation/reallocation
|
|
136
153
|
rb_funcall(cstream->io, id_write, 1, outBuffer);
|
|
137
154
|
}
|
|
138
155
|
}
|
|
139
156
|
|
|
157
|
+
return Qnil;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Ensure function: always unlocks data regardless of raise/return
|
|
161
|
+
static VALUE
|
|
162
|
+
vibe_zstd_writer_write_unlock(VALUE arg) {
|
|
163
|
+
rb_str_unlocktmp((VALUE)arg);
|
|
164
|
+
return Qnil;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static VALUE
|
|
168
|
+
vibe_zstd_writer_write(VALUE self, VALUE data) {
|
|
169
|
+
Check_Type(data, T_STRING);
|
|
170
|
+
|
|
171
|
+
vibe_zstd_cstream* cstream;
|
|
172
|
+
TypedData_Get_Struct(self, vibe_zstd_cstream, &vibe_zstd_cstream_type, cstream);
|
|
173
|
+
|
|
174
|
+
// Lock data for the duration of the compress loop so that RSTRING_PTR(data)
|
|
175
|
+
// stays valid even when io.write (called inside the loop) runs Ruby code that
|
|
176
|
+
// could otherwise mutate or resize the string. rb_str_locktmp raises if the
|
|
177
|
+
// string is already locked; the ensure always unlocks it.
|
|
178
|
+
rb_str_locktmp(data);
|
|
179
|
+
|
|
180
|
+
vibe_zstd_write_state state = { cstream, data };
|
|
181
|
+
rb_ensure(vibe_zstd_writer_write_body, (VALUE)&state,
|
|
182
|
+
vibe_zstd_writer_write_unlock, data);
|
|
183
|
+
|
|
140
184
|
return self;
|
|
141
185
|
}
|
|
142
186
|
|
|
@@ -154,7 +198,9 @@ vibe_zstd_writer_flush(VALUE self) {
|
|
|
154
198
|
// ZSTD_e_flush: flush internal buffers, making all data readable
|
|
155
199
|
// Loop until remaining == 0 (flush complete)
|
|
156
200
|
do {
|
|
157
|
-
|
|
201
|
+
rb_str_modify(outBuffer);
|
|
202
|
+
rb_str_resize(outBuffer, (long)outBufferSize);
|
|
203
|
+
rb_str_set_len(outBuffer, 0);
|
|
158
204
|
ZSTD_outBuffer output = {
|
|
159
205
|
.dst = RSTRING_PTR(outBuffer),
|
|
160
206
|
.size = outBufferSize,
|
|
@@ -190,7 +236,9 @@ vibe_zstd_writer_finish(VALUE self) {
|
|
|
190
236
|
// ZSTD_e_end: finalize frame with checksum and epilogue
|
|
191
237
|
// Loop until remaining == 0 (frame complete)
|
|
192
238
|
do {
|
|
193
|
-
|
|
239
|
+
rb_str_modify(outBuffer);
|
|
240
|
+
rb_str_resize(outBuffer, (long)outBufferSize);
|
|
241
|
+
rb_str_set_len(outBuffer, 0);
|
|
194
242
|
ZSTD_outBuffer output = {
|
|
195
243
|
.dst = RSTRING_PTR(outBuffer),
|
|
196
244
|
.size = outBufferSize,
|
|
@@ -267,6 +315,9 @@ vibe_zstd_reader_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
267
315
|
if (ZSTD_isError(result)) {
|
|
268
316
|
rb_raise(rb_eRuntimeError, "Failed to set dictionary: %s", ZSTD_getErrorName(result));
|
|
269
317
|
}
|
|
318
|
+
// Retain the DDict object so GC won't free it while the stream holds a raw
|
|
319
|
+
// pointer to its internal ZSTD_DDict (ZSTD_DCtx_refDDict stores no Ruby ref)
|
|
320
|
+
rb_ivar_set(self, rb_intern("@dict"), dict);
|
|
270
321
|
}
|
|
271
322
|
|
|
272
323
|
// Initialize input buffer management
|
|
@@ -290,10 +341,18 @@ vibe_zstd_reader_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
290
341
|
// - Maintains internal compressed input buffer that refills from IO as needed
|
|
291
342
|
// - Calls ZSTD_decompressStream incrementally to produce output
|
|
292
343
|
// - Tracks EOF state based on IO exhaustion and frame completion
|
|
344
|
+
// - Input chunks are stored as frozen copies so that IOs which mutate/reuse
|
|
345
|
+
// the returned string cannot invalidate dstream->input.src between calls
|
|
293
346
|
//
|
|
294
347
|
// EOF handling:
|
|
295
348
|
// - Returns nil when no more data available
|
|
296
349
|
// - Sets eof flag when: IO returns nil, frame complete (ret==0), or no progress made
|
|
350
|
+
// - read(0) always returns "" immediately without touching stream state
|
|
351
|
+
//
|
|
352
|
+
// Allocation strategy:
|
|
353
|
+
// - Initial buffer is capped at ZSTD_DStreamOutSize() to avoid gigabyte
|
|
354
|
+
// allocations for large size arguments on small streams
|
|
355
|
+
// - Buffer grows geometrically (doubling) up to requested_size as needed
|
|
297
356
|
//
|
|
298
357
|
// This implements proper streaming semantics for incremental decompression
|
|
299
358
|
// of arbitrarily large files without loading everything into memory.
|
|
@@ -305,6 +364,11 @@ vibe_zstd_reader_read(int argc, VALUE *argv, VALUE self) {
|
|
|
305
364
|
vibe_zstd_dstream* dstream;
|
|
306
365
|
TypedData_Get_Struct(self, vibe_zstd_dstream, &vibe_zstd_dstream_type, dstream);
|
|
307
366
|
|
|
367
|
+
// read(0): per IO semantics, always return "" without touching stream state
|
|
368
|
+
if (!NIL_P(size_arg) && NUM2SIZET(size_arg) == 0) {
|
|
369
|
+
return rb_str_new(NULL, 0);
|
|
370
|
+
}
|
|
371
|
+
|
|
308
372
|
if (dstream->eof) {
|
|
309
373
|
return Qnil;
|
|
310
374
|
}
|
|
@@ -315,8 +379,12 @@ vibe_zstd_reader_read(int argc, VALUE *argv, VALUE self) {
|
|
|
315
379
|
size_t requested_size = NIL_P(size_arg) ? default_chunk_size : NUM2SIZET(size_arg);
|
|
316
380
|
size_t inBufferSize = ZSTD_DStreamInSize();
|
|
317
381
|
|
|
318
|
-
//
|
|
319
|
-
|
|
382
|
+
// Cap the initial allocation to avoid multi-gigabyte pre-allocations when
|
|
383
|
+
// the caller passes a huge size argument for a small stream. The buffer
|
|
384
|
+
// grows geometrically below as output accumulates.
|
|
385
|
+
size_t default_out_size = ZSTD_DStreamOutSize();
|
|
386
|
+
size_t initial_alloc = (requested_size < default_out_size) ? requested_size : default_out_size;
|
|
387
|
+
VALUE result = rb_str_buf_new((long)initial_alloc);
|
|
320
388
|
|
|
321
389
|
size_t total_read = 0;
|
|
322
390
|
int made_progress = 0;
|
|
@@ -333,10 +401,21 @@ vibe_zstd_reader_read(int argc, VALUE *argv, VALUE self) {
|
|
|
333
401
|
break;
|
|
334
402
|
}
|
|
335
403
|
|
|
404
|
+
// The IO is duck-typed: read may return anything. Convert via to_str
|
|
405
|
+
// (raising TypeError otherwise) so RSTRING below never sees a non-String.
|
|
406
|
+
StringValue(chunk);
|
|
407
|
+
|
|
408
|
+
// Store a private frozen copy so that an IO that reuses/mutates its
|
|
409
|
+
// returned buffer string cannot invalidate dstream->input.src between
|
|
410
|
+
// successive read() calls. rb_str_new_frozen is cheap (copy-on-write
|
|
411
|
+
// snapshot) when the string is already frozen, and allocates a
|
|
412
|
+
// separate copy otherwise.
|
|
413
|
+
VALUE frozen_chunk = rb_str_new_frozen(chunk);
|
|
414
|
+
|
|
336
415
|
// Reset input buffer with new data (write barrier for WB_PROTECTED)
|
|
337
|
-
RB_OBJ_WRITE(self, &dstream->input_data,
|
|
338
|
-
dstream->input.src = RSTRING_PTR(
|
|
339
|
-
dstream->input.size = RSTRING_LEN(
|
|
416
|
+
RB_OBJ_WRITE(self, &dstream->input_data, frozen_chunk);
|
|
417
|
+
dstream->input.src = RSTRING_PTR(frozen_chunk);
|
|
418
|
+
dstream->input.size = RSTRING_LEN(frozen_chunk);
|
|
340
419
|
dstream->input.pos = 0;
|
|
341
420
|
}
|
|
342
421
|
|
|
@@ -345,7 +424,22 @@ vibe_zstd_reader_read(int argc, VALUE *argv, VALUE self) {
|
|
|
345
424
|
break;
|
|
346
425
|
}
|
|
347
426
|
|
|
348
|
-
|
|
427
|
+
// Grow the output buffer geometrically when it is full, capped at
|
|
428
|
+
// requested_size. We must recompute RSTRING_PTR after any resize
|
|
429
|
+
// because the backing allocation may move.
|
|
430
|
+
size_t current_capacity = (size_t)rb_str_capacity(result);
|
|
431
|
+
if (total_read >= current_capacity) {
|
|
432
|
+
size_t new_capacity = current_capacity * 2;
|
|
433
|
+
if (new_capacity > requested_size) new_capacity = requested_size;
|
|
434
|
+
rb_str_resize(result, (long)new_capacity);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Cap space_left at (requested_size - total_read) to ensure read(n) never
|
|
438
|
+
// returns more than n bytes: rb_str_capacity may exceed the requested size
|
|
439
|
+
// due to malloc's internal size-class rounding (e.g. request 100, get 135).
|
|
440
|
+
size_t effective_capacity = (size_t)rb_str_capacity(result);
|
|
441
|
+
if (effective_capacity > requested_size) effective_capacity = requested_size;
|
|
442
|
+
size_t space_left = effective_capacity - total_read;
|
|
349
443
|
|
|
350
444
|
ZSTD_outBuffer output = {
|
|
351
445
|
.dst = RSTRING_PTR(result) + total_read,
|
data/ext/vibe_zstd/vibe_zstd.c
CHANGED
|
@@ -210,6 +210,7 @@ vibe_zstd_dctx_alloc(VALUE klass) {
|
|
|
210
210
|
rb_raise(rb_eRuntimeError, "Failed to create ZSTD_DCtx");
|
|
211
211
|
}
|
|
212
212
|
dctx->initial_capacity = 0; // 0 = use class default
|
|
213
|
+
dctx->max_decompressed_size = 0; // 0 = inherit class default
|
|
213
214
|
return TypedData_Wrap_Struct(klass, &vibe_zstd_dctx_type, dctx);
|
|
214
215
|
}
|
|
215
216
|
|
|
@@ -279,6 +280,35 @@ vibe_zstd_default_c_level(VALUE self) {
|
|
|
279
280
|
return INT2NUM(ZSTD_defaultCLevel());
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
// Run func(arg) without the GVL while str is locked against mutation.
|
|
284
|
+
// rb_thread_call_without_gvl can deliver a pending async exception (e.g.
|
|
285
|
+
// Thread#raise, Timeout) after reacquiring the GVL, so the unlock must go
|
|
286
|
+
// through rb_ensure or the string would be left permanently locked.
|
|
287
|
+
typedef struct {
|
|
288
|
+
void* (*func)(void*);
|
|
289
|
+
void* arg;
|
|
290
|
+
} nogvl_locked_call;
|
|
291
|
+
|
|
292
|
+
static VALUE
|
|
293
|
+
nogvl_locked_body(VALUE p) {
|
|
294
|
+
nogvl_locked_call* call = (nogvl_locked_call*)p;
|
|
295
|
+
rb_thread_call_without_gvl(call->func, call->arg, NULL, NULL);
|
|
296
|
+
return Qnil;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
static VALUE
|
|
300
|
+
nogvl_locked_unlock(VALUE str) {
|
|
301
|
+
rb_str_unlocktmp(str);
|
|
302
|
+
return Qnil;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
static void
|
|
306
|
+
vibe_zstd_nogvl_with_str_locked(void* (*func)(void*), void* arg, VALUE str) {
|
|
307
|
+
nogvl_locked_call call = { func, arg };
|
|
308
|
+
rb_str_locktmp(str);
|
|
309
|
+
rb_ensure(nogvl_locked_body, (VALUE)&call, nogvl_locked_unlock, str);
|
|
310
|
+
}
|
|
311
|
+
|
|
282
312
|
// Include the split implementation files
|
|
283
313
|
#include "cctx.c"
|
|
284
314
|
#include "dctx.c"
|
data/ext/vibe_zstd/vibe_zstd.h
CHANGED
|
@@ -13,6 +13,7 @@ typedef struct {
|
|
|
13
13
|
typedef struct {
|
|
14
14
|
ZSTD_DCtx* dctx;
|
|
15
15
|
size_t initial_capacity; // Initial capacity for unknown-size decompression (0 = use class default)
|
|
16
|
+
size_t max_decompressed_size; // Output size limit (0 = inherit class default; class default 0 = unlimited)
|
|
16
17
|
} vibe_zstd_dctx;
|
|
17
18
|
|
|
18
19
|
typedef struct {
|
data/lib/vibe_zstd/version.rb
CHANGED
data/lib/vibe_zstd.rb
CHANGED
|
@@ -7,18 +7,33 @@ require_relative "vibe_zstd/constants"
|
|
|
7
7
|
module VibeZstd
|
|
8
8
|
class Error < StandardError; end
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# Keyword options handled per-operation by CCtx#compress / DCtx#decompress.
|
|
11
|
+
# Any other keyword is treated as a context (sticky) parameter and applied via
|
|
12
|
+
# the constructor, so e.g. VibeZstd.compress(data, checksum_flag: true) and
|
|
13
|
+
# VibeZstd.decompress(data, format: 1) work through the convenience methods.
|
|
14
|
+
# An unknown keyword raises NoMethodError from the corresponding setter.
|
|
15
|
+
COMPRESS_CALL_OPTIONS = %i[level dict pledged_size].freeze
|
|
16
|
+
DECOMPRESS_CALL_OPTIONS = %i[dict initial_capacity max_decompressed_size max_size].freeze
|
|
17
|
+
private_constant :COMPRESS_CALL_OPTIONS, :DECOMPRESS_CALL_OPTIONS
|
|
18
|
+
|
|
19
|
+
# Convenience method for one-off compression.
|
|
20
|
+
# Per-call options (level, dict, pledged_size) are passed to #compress; any
|
|
21
|
+
# other keyword is a context parameter (e.g. checksum_flag:, window_log:,
|
|
22
|
+
# workers:, format:) applied to a fresh CCtx.
|
|
12
23
|
def self.compress(data, **options)
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
call_opts = options.slice(*COMPRESS_CALL_OPTIONS)
|
|
25
|
+
ctx_opts = options.except(*COMPRESS_CALL_OPTIONS)
|
|
26
|
+
CCtx.new(**ctx_opts).compress(data, **call_opts)
|
|
15
27
|
end
|
|
16
28
|
|
|
17
|
-
# Convenience method for one-off decompression
|
|
18
|
-
#
|
|
29
|
+
# Convenience method for one-off decompression.
|
|
30
|
+
# Per-call options (dict, initial_capacity, max_decompressed_size/max_size) are
|
|
31
|
+
# passed to #decompress; any other keyword is a context parameter (e.g.
|
|
32
|
+
# format:, window_log_max:) applied to a fresh DCtx.
|
|
19
33
|
def self.decompress(data, **options)
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
call_opts = options.slice(*DECOMPRESS_CALL_OPTIONS)
|
|
35
|
+
ctx_opts = options.except(*DECOMPRESS_CALL_OPTIONS)
|
|
36
|
+
DCtx.new(**ctx_opts).decompress(data, **call_opts)
|
|
22
37
|
end
|
|
23
38
|
|
|
24
39
|
# Get the decompressed content size from a compressed frame
|
|
@@ -88,6 +103,10 @@ module VibeZstd
|
|
|
88
103
|
# Memory footprint: ~128KB per DCtx × unique dictionaries × threads
|
|
89
104
|
# Example: 3 dicts × 5 Puma threads = 1.9MB total
|
|
90
105
|
#
|
|
106
|
+
# Storage: uses Thread#thread_variable_get/set (true thread-local) so that
|
|
107
|
+
# fiber-based servers (Falcon, async) share one pool per OS thread rather
|
|
108
|
+
# than allocating a fresh pool for every fiber.
|
|
109
|
+
#
|
|
91
110
|
# Note: Only supports per-operation parameters (level, dict, pledged_size, initial_capacity)
|
|
92
111
|
# Does NOT support context-level settings (nb_workers, checksum_flag, etc.)
|
|
93
112
|
module ThreadLocal
|
|
@@ -103,9 +122,10 @@ module VibeZstd
|
|
|
103
122
|
# Key by dictionary ID, or :default if no dict
|
|
104
123
|
key = dict ? dict.dict_id : :default
|
|
105
124
|
|
|
106
|
-
# Get or create thread-local context pool
|
|
107
|
-
Thread.current
|
|
108
|
-
cctx =
|
|
125
|
+
# Get or create thread-local context pool (true thread-local, not fiber-local)
|
|
126
|
+
pool = Thread.current.thread_variable_get(:vibe_zstd_cctx_pool) || {}
|
|
127
|
+
cctx = pool[key] ||= VibeZstd::CCtx.new
|
|
128
|
+
Thread.current.thread_variable_set(:vibe_zstd_cctx_pool, pool)
|
|
109
129
|
|
|
110
130
|
# Build options hash
|
|
111
131
|
options = {}
|
|
@@ -122,18 +142,21 @@ module VibeZstd
|
|
|
122
142
|
# @param data [String] Data to decompress
|
|
123
143
|
# @param dict [DDict] Decompression dictionary (optional)
|
|
124
144
|
# @param initial_capacity [Integer] Initial buffer size for unknown-size frames (optional)
|
|
145
|
+
# @param max_decompressed_size [Integer] Output-size limit; raises DecompressedSizeExceeded if exceeded (optional)
|
|
125
146
|
# @return [String] Decompressed data
|
|
126
|
-
def self.decompress(data, dict: nil, initial_capacity: nil)
|
|
147
|
+
def self.decompress(data, dict: nil, initial_capacity: nil, max_decompressed_size: nil)
|
|
127
148
|
key = dict ? dict.dict_id : :default
|
|
128
149
|
|
|
129
|
-
# Get or create thread-local context pool
|
|
130
|
-
Thread.current
|
|
131
|
-
dctx =
|
|
150
|
+
# Get or create thread-local context pool (true thread-local, not fiber-local)
|
|
151
|
+
pool = Thread.current.thread_variable_get(:vibe_zstd_dctx_pool) || {}
|
|
152
|
+
dctx = pool[key] ||= VibeZstd::DCtx.new
|
|
153
|
+
Thread.current.thread_variable_set(:vibe_zstd_dctx_pool, pool)
|
|
132
154
|
|
|
133
155
|
# Build options hash
|
|
134
156
|
options = {}
|
|
135
157
|
options[:dict] = dict if dict
|
|
136
158
|
options[:initial_capacity] = initial_capacity if initial_capacity
|
|
159
|
+
options[:max_decompressed_size] = max_decompressed_size if max_decompressed_size
|
|
137
160
|
|
|
138
161
|
# C code will validate dict matches frame requirements
|
|
139
162
|
dctx.decompress(data, **options)
|
|
@@ -142,19 +165,21 @@ module VibeZstd
|
|
|
142
165
|
# Clear all thread-local context pools for the current thread
|
|
143
166
|
# Useful for testing or explicit memory management
|
|
144
167
|
def self.clear_thread_cache!
|
|
145
|
-
Thread.current
|
|
146
|
-
Thread.current
|
|
168
|
+
Thread.current.thread_variable_set(:vibe_zstd_cctx_pool, {})
|
|
169
|
+
Thread.current.thread_variable_set(:vibe_zstd_dctx_pool, {})
|
|
147
170
|
nil
|
|
148
171
|
end
|
|
149
172
|
|
|
150
173
|
# Get statistics about the current thread's context pools
|
|
151
174
|
# @return [Hash] Pool statistics
|
|
152
175
|
def self.thread_cache_stats
|
|
176
|
+
cctx_pool = Thread.current.thread_variable_get(:vibe_zstd_cctx_pool)
|
|
177
|
+
dctx_pool = Thread.current.thread_variable_get(:vibe_zstd_dctx_pool)
|
|
153
178
|
{
|
|
154
|
-
compression_contexts:
|
|
155
|
-
decompression_contexts:
|
|
156
|
-
compression_keys:
|
|
157
|
-
decompression_keys:
|
|
179
|
+
compression_contexts: cctx_pool&.size || 0,
|
|
180
|
+
decompression_contexts: dctx_pool&.size || 0,
|
|
181
|
+
compression_keys: cctx_pool&.keys || [],
|
|
182
|
+
decompression_keys: dctx_pool&.keys || []
|
|
158
183
|
}
|
|
159
184
|
end
|
|
160
185
|
end
|
|
@@ -229,7 +254,7 @@ module VibeZstd
|
|
|
229
254
|
loop do
|
|
230
255
|
# Check buffer for separator
|
|
231
256
|
if (idx = @line_buffer.index(sep))
|
|
232
|
-
return @line_buffer.slice!(0, idx + sep.
|
|
257
|
+
return @line_buffer.slice!(0, idx + sep.length)
|
|
233
258
|
end
|
|
234
259
|
|
|
235
260
|
# Read more data in larger chunks
|
|
@@ -240,7 +265,7 @@ module VibeZstd
|
|
|
240
265
|
end
|
|
241
266
|
|
|
242
267
|
# Return remaining buffer or nil
|
|
243
|
-
@line_buffer.empty? ? nil : @line_buffer.slice!(0, @line_buffer.
|
|
268
|
+
@line_buffer.empty? ? nil : @line_buffer.slice!(0, @line_buffer.length)
|
|
244
269
|
end
|
|
245
270
|
|
|
246
271
|
# Iterate over lines
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vibe_zstd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kelley Reynolds
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-06-12 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: benchmark-ips
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- benchmark/streaming.rb
|
|
65
65
|
- ext/vibe_zstd/cctx.c
|
|
66
66
|
- ext/vibe_zstd/dctx.c
|
|
67
|
+
- ext/vibe_zstd/depend
|
|
67
68
|
- ext/vibe_zstd/dict.c
|
|
68
69
|
- ext/vibe_zstd/extconf.rb
|
|
69
70
|
- ext/vibe_zstd/frames.c
|