fastsimdjson 0.4.0__tar.gz → 0.5.0__tar.gz
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.
- {fastsimdjson-0.4.0/src/fastsimdjson.egg-info → fastsimdjson-0.5.0}/PKG-INFO +53 -1
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/README.md +52 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/pyproject.toml +1 -1
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/src/fastsimdjson.cpp +425 -7
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0/src/fastsimdjson.egg-info}/PKG-INFO +53 -1
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/tests/test_stream.py +66 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/MANIFEST.in +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/setup.cfg +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/setup.py +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/src/fastsimdjson.egg-info/SOURCES.txt +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/src/fastsimdjson.egg-info/dependency_links.txt +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/src/fastsimdjson.egg-info/requires.txt +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/src/fastsimdjson.egg-info/top_level.txt +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/tests/test_dumps.py +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/tests/test_files.py +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/tests/test_lazy.py +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/tests/test_loads.py +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/vendor/simdjson.cpp +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/vendor/simdjson.h +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/vendor/simdutf.cpp +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/vendor/simdutf.h +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/vendor/simdutf_c.h +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/vendor/zmij-LICENSE +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/vendor/zmij.cc +0 -0
- {fastsimdjson-0.4.0 → fastsimdjson-0.5.0}/vendor/zmij.h +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastsimdjson
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Fast JSON parsing for Python, built on simdjson
|
|
5
5
|
Author-email: Daniel Lemire <daniel@lemire.me>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -197,6 +197,40 @@ returned, otherwise `JSONDecodeError` reports `json`'s message and the
|
|
|
197
197
|
position in the whole input. A truncated last document is an error. The
|
|
198
198
|
views returned by `parse_many` remain valid after the iterator moves on.
|
|
199
199
|
|
|
200
|
+
### Streams on several threads: `threads=N` and `map_many`
|
|
201
|
+
|
|
202
|
+
On free-threaded Python (3.14t and newer), a stream can be parsed by several
|
|
203
|
+
threads at once:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
docs = fastsimdjson.loads_many(data, threads=16) # a list, in order
|
|
207
|
+
views = fastsimdjson.parse_many(data, threads=16) # a list of views
|
|
208
|
+
# fn runs in the threads too, on each document; its results, in order:
|
|
209
|
+
scores = fastsimdjson.map_many(score, data, threads=16)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
With `threads`, `loads_many` and `parse_many` return a list of all the
|
|
213
|
+
documents instead of an iterator. `map_many(fn, data, **kw)` returns
|
|
214
|
+
`[fn(doc) for doc in loads_many(data, **kw)]` (`parse_many` with
|
|
215
|
+
`lazy=True`), with `fn` called in the threads. `threads=0` (or `None`) uses
|
|
216
|
+
one thread per processor; small inputs use fewer threads (about one per
|
|
217
|
+
512 KB). The input is cut into chunks at newlines (before each `\x1e` for
|
|
218
|
+
`json_seq`), so `threads` works with the formats `"whitespace"`, `"lines"`
|
|
219
|
+
and `"json_seq"`.
|
|
220
|
+
|
|
221
|
+
The results, and the exceptions, are those of one thread: when a chunk
|
|
222
|
+
contains an error or a document that only `json` accepts, the documents
|
|
223
|
+
before it are kept and the rest is parsed in the calling thread, as without
|
|
224
|
+
threads. An exception raised by `fn` is raised again, for the first document
|
|
225
|
+
where it happened; `fn` may then have been called on later documents, in
|
|
226
|
+
other threads, and it must be safe to call from several threads at once.
|
|
227
|
+
|
|
228
|
+
`map_many` scales best: each document is built, used and freed in the same
|
|
229
|
+
thread, and only `fn`'s results come back. `loads_many` must return every
|
|
230
|
+
document, and the Python objects of all of them, to the calling thread.
|
|
231
|
+
With the GIL (the default build of Python), the calling thread does all the
|
|
232
|
+
work: `threads` has no effect on speed.
|
|
233
|
+
|
|
200
234
|
### `release`
|
|
201
235
|
|
|
202
236
|
`release()` frees the simdjson parser and the string caches kept by the
|
|
@@ -433,6 +467,24 @@ msgspec's `decode_lines` and `loads_many` are on par (`loads_many` is about 2%
|
|
|
433
467
|
faster). `parse_many` only creates the views; reading values from them adds to
|
|
434
468
|
its time.
|
|
435
469
|
|
|
470
|
+
### Streams on several threads
|
|
471
|
+
|
|
472
|
+
300 MB of NDJSON (146,624 records: tweets, GitHub events, citm performances,
|
|
473
|
+
gsoc and update-center entries), free-threaded Python 3.14.6, collector
|
|
474
|
+
disabled (see below), Intel Xeon Gold 6548N (64 cores); GB/s:
|
|
475
|
+
|
|
476
|
+
| threads | 1 | 2 | 4 | 8 | 16 | 32 | 64 |
|
|
477
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
478
|
+
| `map_many(len, data, threads=N)` | 0.91 | 1.77 | 3.28 | 6.22 | 10.9 | 15.4 | **21.2** |
|
|
479
|
+
| `parse_many(data, threads=N)` | 1.82 | 3.06 | 4.80 | 6.90 | 10.2 | 12.3 | 16.4 |
|
|
480
|
+
| `loads_many(data, threads=N)` | 0.82 | 1.43 | 2.26 | 3.88 | 5.77 | 7.45 | 8.63 |
|
|
481
|
+
|
|
482
|
+
`map_many` is 23 times faster on 64 threads than on one. `loads_many` builds
|
|
483
|
+
every document for the calling thread and is 11 times faster. With the
|
|
484
|
+
collector enabled, Python collects after the call returns: it examines the
|
|
485
|
+
millions of objects just created, which takes about 0.1 s here, with or
|
|
486
|
+
without threads.
|
|
487
|
+
|
|
436
488
|
## Limitations
|
|
437
489
|
|
|
438
490
|
* The simdjson parser and the key and string caches are thread-local.
|
|
@@ -166,6 +166,40 @@ returned, otherwise `JSONDecodeError` reports `json`'s message and the
|
|
|
166
166
|
position in the whole input. A truncated last document is an error. The
|
|
167
167
|
views returned by `parse_many` remain valid after the iterator moves on.
|
|
168
168
|
|
|
169
|
+
### Streams on several threads: `threads=N` and `map_many`
|
|
170
|
+
|
|
171
|
+
On free-threaded Python (3.14t and newer), a stream can be parsed by several
|
|
172
|
+
threads at once:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
docs = fastsimdjson.loads_many(data, threads=16) # a list, in order
|
|
176
|
+
views = fastsimdjson.parse_many(data, threads=16) # a list of views
|
|
177
|
+
# fn runs in the threads too, on each document; its results, in order:
|
|
178
|
+
scores = fastsimdjson.map_many(score, data, threads=16)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
With `threads`, `loads_many` and `parse_many` return a list of all the
|
|
182
|
+
documents instead of an iterator. `map_many(fn, data, **kw)` returns
|
|
183
|
+
`[fn(doc) for doc in loads_many(data, **kw)]` (`parse_many` with
|
|
184
|
+
`lazy=True`), with `fn` called in the threads. `threads=0` (or `None`) uses
|
|
185
|
+
one thread per processor; small inputs use fewer threads (about one per
|
|
186
|
+
512 KB). The input is cut into chunks at newlines (before each `\x1e` for
|
|
187
|
+
`json_seq`), so `threads` works with the formats `"whitespace"`, `"lines"`
|
|
188
|
+
and `"json_seq"`.
|
|
189
|
+
|
|
190
|
+
The results, and the exceptions, are those of one thread: when a chunk
|
|
191
|
+
contains an error or a document that only `json` accepts, the documents
|
|
192
|
+
before it are kept and the rest is parsed in the calling thread, as without
|
|
193
|
+
threads. An exception raised by `fn` is raised again, for the first document
|
|
194
|
+
where it happened; `fn` may then have been called on later documents, in
|
|
195
|
+
other threads, and it must be safe to call from several threads at once.
|
|
196
|
+
|
|
197
|
+
`map_many` scales best: each document is built, used and freed in the same
|
|
198
|
+
thread, and only `fn`'s results come back. `loads_many` must return every
|
|
199
|
+
document, and the Python objects of all of them, to the calling thread.
|
|
200
|
+
With the GIL (the default build of Python), the calling thread does all the
|
|
201
|
+
work: `threads` has no effect on speed.
|
|
202
|
+
|
|
169
203
|
### `release`
|
|
170
204
|
|
|
171
205
|
`release()` frees the simdjson parser and the string caches kept by the
|
|
@@ -402,6 +436,24 @@ msgspec's `decode_lines` and `loads_many` are on par (`loads_many` is about 2%
|
|
|
402
436
|
faster). `parse_many` only creates the views; reading values from them adds to
|
|
403
437
|
its time.
|
|
404
438
|
|
|
439
|
+
### Streams on several threads
|
|
440
|
+
|
|
441
|
+
300 MB of NDJSON (146,624 records: tweets, GitHub events, citm performances,
|
|
442
|
+
gsoc and update-center entries), free-threaded Python 3.14.6, collector
|
|
443
|
+
disabled (see below), Intel Xeon Gold 6548N (64 cores); GB/s:
|
|
444
|
+
|
|
445
|
+
| threads | 1 | 2 | 4 | 8 | 16 | 32 | 64 |
|
|
446
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
447
|
+
| `map_many(len, data, threads=N)` | 0.91 | 1.77 | 3.28 | 6.22 | 10.9 | 15.4 | **21.2** |
|
|
448
|
+
| `parse_many(data, threads=N)` | 1.82 | 3.06 | 4.80 | 6.90 | 10.2 | 12.3 | 16.4 |
|
|
449
|
+
| `loads_many(data, threads=N)` | 0.82 | 1.43 | 2.26 | 3.88 | 5.77 | 7.45 | 8.63 |
|
|
450
|
+
|
|
451
|
+
`map_many` is 23 times faster on 64 threads than on one. `loads_many` builds
|
|
452
|
+
every document for the calling thread and is 11 times faster. With the
|
|
453
|
+
collector enabled, Python collects after the call returns: it examines the
|
|
454
|
+
millions of objects just created, which takes about 0.1 s here, with or
|
|
455
|
+
without threads.
|
|
456
|
+
|
|
405
457
|
## Limitations
|
|
406
458
|
|
|
407
459
|
* The simdjson parser and the key and string caches are thread-local.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
#include <cstring>
|
|
12
12
|
#include <new>
|
|
13
13
|
#include <string>
|
|
14
|
+
#include <thread>
|
|
14
15
|
#include <vector>
|
|
15
16
|
#ifdef _WIN32
|
|
16
17
|
#ifndef NOMINMAX
|
|
@@ -3052,6 +3053,9 @@ struct StreamObject {
|
|
|
3052
3053
|
size_t open; // array format: offsets of the outer '[' and ']'
|
|
3053
3054
|
size_t close;
|
|
3054
3055
|
PyObject *text; // the input as str, built on the error path
|
|
3056
|
+
size_t text_len; // bytes of buf that `text` covers (len, or more for a chunk)
|
|
3057
|
+
bool no_fallback; // a chunk of a parallel run: do not call json (see run_chunk)
|
|
3058
|
+
bool need_fallback; // set instead of calling json, with no_fallback
|
|
3055
3059
|
parser *p;
|
|
3056
3060
|
document_stream *stream;
|
|
3057
3061
|
document_stream::iterator it;
|
|
@@ -3111,7 +3115,7 @@ PyObject *stream_text(StreamObject *s) {
|
|
|
3111
3115
|
if (s->text == nullptr) {
|
|
3112
3116
|
s->text = PyUnicode_Check(s->source)
|
|
3113
3117
|
? Py_NewRef(s->source)
|
|
3114
|
-
: PyUnicode_DecodeUTF8(s->buf, Py_ssize_t(s->
|
|
3118
|
+
: PyUnicode_DecodeUTF8(s->buf, Py_ssize_t(s->text_len), nullptr);
|
|
3115
3119
|
}
|
|
3116
3120
|
return s->text;
|
|
3117
3121
|
}
|
|
@@ -3252,6 +3256,16 @@ PyObject *stream_fallback(StreamObject *s) {
|
|
|
3252
3256
|
return value;
|
|
3253
3257
|
}
|
|
3254
3258
|
|
|
3259
|
+
// In a chunk of a parallel run, json is not called: the run continues in one
|
|
3260
|
+
// thread from there, where json decides as it does without threads.
|
|
3261
|
+
PyObject *stream_fallback_or_stop(StreamObject *s) {
|
|
3262
|
+
if (s->no_fallback) {
|
|
3263
|
+
s->need_fallback = true;
|
|
3264
|
+
return nullptr;
|
|
3265
|
+
}
|
|
3266
|
+
return stream_fallback(s);
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3255
3269
|
// The next document, or nullptr at the end or after an error.
|
|
3256
3270
|
PyObject *stream_next_locked(StreamObject *s) {
|
|
3257
3271
|
for (;;) {
|
|
@@ -3266,7 +3280,7 @@ PyObject *stream_next_locked(StreamObject *s) {
|
|
|
3266
3280
|
if (!(s->it != s->stream->end())) {
|
|
3267
3281
|
// Anything but separators after the last document is an incomplete
|
|
3268
3282
|
// document: let json report it.
|
|
3269
|
-
return skip_separators(s, s->last_end) < s->len ?
|
|
3283
|
+
return skip_separators(s, s->last_end) < s->len ? stream_fallback_or_stop(s) : nullptr;
|
|
3270
3284
|
}
|
|
3271
3285
|
simdjson::dom::element el;
|
|
3272
3286
|
simdjson::error_code err = (*s->it).get(el);
|
|
@@ -3285,7 +3299,7 @@ PyObject *stream_next_locked(StreamObject *s) {
|
|
|
3285
3299
|
s->active = false;
|
|
3286
3300
|
continue;
|
|
3287
3301
|
}
|
|
3288
|
-
return
|
|
3302
|
+
return stream_fallback_or_stop(s);
|
|
3289
3303
|
}
|
|
3290
3304
|
}
|
|
3291
3305
|
|
|
@@ -3393,6 +3407,7 @@ PyObject *make_stream(PyObject *args, PyObject *kwargs, bool lazy) {
|
|
|
3393
3407
|
s->active = false;
|
|
3394
3408
|
s->base = 0;
|
|
3395
3409
|
s->last_end = 0;
|
|
3410
|
+
s->no_fallback = s->need_fallback = false;
|
|
3396
3411
|
PyObject *self = reinterpret_cast<PyObject *>(s);
|
|
3397
3412
|
if (must_copy) {
|
|
3398
3413
|
PyObject *b = PyBytes_FromObject(data);
|
|
@@ -3415,7 +3430,7 @@ PyObject *make_stream(PyObject *args, PyObject *kwargs, bool lazy) {
|
|
|
3415
3430
|
buf = s->copy->data();
|
|
3416
3431
|
}
|
|
3417
3432
|
s->buf = buf;
|
|
3418
|
-
s->len = size_t(len);
|
|
3433
|
+
s->len = s->text_len = size_t(len);
|
|
3419
3434
|
// parse_many skips a UTF-8 byte order mark and, for the array format,
|
|
3420
3435
|
// white space and the opening '['; the offsets it reports start after.
|
|
3421
3436
|
size_t i = s->len >= 3 && memcmp(buf, "\xEF\xBB\xBF", 3) == 0 ? 3 : 0;
|
|
@@ -3451,12 +3466,410 @@ PyObject *make_stream(PyObject *args, PyObject *kwargs, bool lazy) {
|
|
|
3451
3466
|
return self;
|
|
3452
3467
|
}
|
|
3453
3468
|
|
|
3469
|
+
// ---------------------------------------------------------------------------
|
|
3470
|
+
// Streams over several threads (threads=N): the input is cut into chunks at
|
|
3471
|
+
// document boundaries (after a newline; before a record separator for
|
|
3472
|
+
// json_seq), and threads take the chunks in turn, each building the
|
|
3473
|
+
// documents of its chunk (and calling fn on them, for map_many). Without
|
|
3474
|
+
// the GIL (free-threaded Python), the threads run in parallel; with it, the
|
|
3475
|
+
// calling thread does all the work.
|
|
3476
|
+
//
|
|
3477
|
+
// The result is that of one thread: at the first chunk where anything other
|
|
3478
|
+
// than plain documents shows up (an error, a document that only json
|
|
3479
|
+
// accepts), the documents before it are kept and the calling thread goes on
|
|
3480
|
+
// from there as a stream without threads would. (fn may then have been
|
|
3481
|
+
// called on documents after that point, in other threads.)
|
|
3482
|
+
// ---------------------------------------------------------------------------
|
|
3483
|
+
|
|
3484
|
+
// A stream over [from, to) of the input of `whole`, sharing its buffer.
|
|
3485
|
+
StreamObject *stream_over(StreamObject *whole, size_t from, size_t to) {
|
|
3486
|
+
StreamObject *s = PyObject_New(StreamObject, StreamType);
|
|
3487
|
+
if (s == nullptr) {
|
|
3488
|
+
return nullptr;
|
|
3489
|
+
}
|
|
3490
|
+
s->source = Py_NewRef(whole->source);
|
|
3491
|
+
s->copy = nullptr;
|
|
3492
|
+
s->text = nullptr;
|
|
3493
|
+
s->stream = nullptr;
|
|
3494
|
+
s->p = nullptr;
|
|
3495
|
+
new (&s->it) document_stream::iterator();
|
|
3496
|
+
s->buf = whole->buf;
|
|
3497
|
+
s->len = to;
|
|
3498
|
+
s->text_len = whole->len;
|
|
3499
|
+
s->batch_size = whole->batch_size;
|
|
3500
|
+
s->format = whole->format;
|
|
3501
|
+
s->lazy = whole->lazy;
|
|
3502
|
+
s->done = false;
|
|
3503
|
+
s->active = false;
|
|
3504
|
+
s->base = 0;
|
|
3505
|
+
s->last_end = from;
|
|
3506
|
+
s->prefix = whole->prefix;
|
|
3507
|
+
s->open = whole->open;
|
|
3508
|
+
s->close = whole->close;
|
|
3509
|
+
s->no_fallback = s->need_fallback = false;
|
|
3510
|
+
s->p = new (std::nothrow) parser();
|
|
3511
|
+
if (s->p == nullptr) {
|
|
3512
|
+
Py_DECREF(reinterpret_cast<PyObject *>(s));
|
|
3513
|
+
PyErr_NoMemory();
|
|
3514
|
+
return nullptr;
|
|
3515
|
+
}
|
|
3516
|
+
s->p->number_as_string(true);
|
|
3517
|
+
return s;
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
struct ParallelRun {
|
|
3521
|
+
StreamObject *whole;
|
|
3522
|
+
PyObject *fn; // borrowed, or nullptr
|
|
3523
|
+
std::vector<size_t> bounds; // chunk i is [bounds[i], bounds[i + 1])
|
|
3524
|
+
struct Chunk {
|
|
3525
|
+
PyObject *items = nullptr; // list of the chunk's results
|
|
3526
|
+
bool failed = false; // stopped before the end of the chunk
|
|
3527
|
+
PyObject *exc = nullptr; // the exception fn raised, if it did
|
|
3528
|
+
size_t resume_at = 0; // where the one-thread run goes on
|
|
3529
|
+
};
|
|
3530
|
+
std::vector<Chunk> chunks;
|
|
3531
|
+
std::atomic<size_t> next{0};
|
|
3532
|
+
std::atomic<size_t> first_failure{SIZE_MAX};
|
|
3533
|
+
|
|
3534
|
+
void note_failure(size_t i) {
|
|
3535
|
+
size_t f = first_failure.load();
|
|
3536
|
+
while (i < f && !first_failure.compare_exchange_weak(f, i)) {
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// On an attached thread.
|
|
3541
|
+
void run_chunk(size_t i) {
|
|
3542
|
+
Chunk &c = chunks[i];
|
|
3543
|
+
c.resume_at = bounds[i];
|
|
3544
|
+
StreamObject *st = stream_over(whole, bounds[i], bounds[i + 1]);
|
|
3545
|
+
c.items = st ? PyList_New(0) : nullptr;
|
|
3546
|
+
if (c.items == nullptr) {
|
|
3547
|
+
PyErr_Clear(); // the one-thread run will report it
|
|
3548
|
+
c.failed = true;
|
|
3549
|
+
}
|
|
3550
|
+
if (st != nullptr && c.items != nullptr) {
|
|
3551
|
+
st->no_fallback = true;
|
|
3552
|
+
while (first_failure.load(std::memory_order_relaxed) > i) {
|
|
3553
|
+
size_t before = st->last_end;
|
|
3554
|
+
PyObject *v = stream_next_locked(st);
|
|
3555
|
+
if (v == nullptr) {
|
|
3556
|
+
if (PyErr_Occurred() || st->need_fallback) {
|
|
3557
|
+
PyErr_Clear(); // reported again by the one-thread run
|
|
3558
|
+
c.failed = true;
|
|
3559
|
+
c.resume_at = before;
|
|
3560
|
+
}
|
|
3561
|
+
break;
|
|
3562
|
+
}
|
|
3563
|
+
c.resume_at = st->last_end;
|
|
3564
|
+
if (fn != nullptr) {
|
|
3565
|
+
PyObject *r = PyObject_CallOneArg(fn, v);
|
|
3566
|
+
Py_DECREF(v);
|
|
3567
|
+
if (r == nullptr) {
|
|
3568
|
+
PyObject *t, *val, *tb;
|
|
3569
|
+
PyErr_Fetch(&t, &val, &tb);
|
|
3570
|
+
PyErr_NormalizeException(&t, &val, &tb);
|
|
3571
|
+
if (tb != nullptr && val != nullptr) {
|
|
3572
|
+
PyException_SetTraceback(val, tb);
|
|
3573
|
+
}
|
|
3574
|
+
Py_XDECREF(t);
|
|
3575
|
+
Py_XDECREF(tb);
|
|
3576
|
+
c.exc = val;
|
|
3577
|
+
c.failed = true;
|
|
3578
|
+
break;
|
|
3579
|
+
}
|
|
3580
|
+
v = r;
|
|
3581
|
+
}
|
|
3582
|
+
int rc = PyList_Append(c.items, v);
|
|
3583
|
+
Py_DECREF(v);
|
|
3584
|
+
if (rc < 0) {
|
|
3585
|
+
PyErr_Clear();
|
|
3586
|
+
c.failed = true;
|
|
3587
|
+
break;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
Py_XDECREF(reinterpret_cast<PyObject *>(st));
|
|
3592
|
+
if (c.failed) {
|
|
3593
|
+
note_failure(i);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
// Takes chunks until none is left, letting the garbage collector of
|
|
3598
|
+
// other threads run between chunks.
|
|
3599
|
+
void work() {
|
|
3600
|
+
for (;;) {
|
|
3601
|
+
size_t i = next.fetch_add(1);
|
|
3602
|
+
if (i >= chunks.size() || i > first_failure.load()) {
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
run_chunk(i);
|
|
3606
|
+
Py_BEGIN_ALLOW_THREADS
|
|
3607
|
+
Py_END_ALLOW_THREADS
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
|
|
3612
|
+
// Chunk boundaries: about `n` chunks, each ending just after a newline (or
|
|
3613
|
+
// just before a record separator, for json_seq).
|
|
3614
|
+
std::vector<size_t> chunk_bounds(const StreamObject *w, size_t n) {
|
|
3615
|
+
std::vector<size_t> b{0};
|
|
3616
|
+
size_t step = std::max<size_t>(w->len / std::max<size_t>(n, 1), 1 << 16);
|
|
3617
|
+
bool seq = w->format == simdjson::stream_format::json_sequence;
|
|
3618
|
+
for (size_t at = step; at < w->len; at = b.back() + step) {
|
|
3619
|
+
const void *hit = memchr(w->buf + at, seq ? '\x1e' : '\n', w->len - at);
|
|
3620
|
+
if (hit == nullptr) {
|
|
3621
|
+
break;
|
|
3622
|
+
}
|
|
3623
|
+
size_t cut = size_t(static_cast<const char *>(hit) - w->buf) + (seq ? 0 : 1);
|
|
3624
|
+
if (cut >= w->len) {
|
|
3625
|
+
break;
|
|
3626
|
+
}
|
|
3627
|
+
b.push_back(cut);
|
|
3628
|
+
}
|
|
3629
|
+
b.push_back(w->len);
|
|
3630
|
+
return b;
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
// All the results (documents, or fn of each) of the stream `whole`, as a
|
|
3634
|
+
// list, using up to `threads` threads.
|
|
3635
|
+
PyObject *run_stream(StreamObject *whole, PyObject *fn, size_t threads) {
|
|
3636
|
+
PyObject *result = PyList_New(0);
|
|
3637
|
+
if (result == nullptr) {
|
|
3638
|
+
return nullptr;
|
|
3639
|
+
}
|
|
3640
|
+
size_t resume_at = 0;
|
|
3641
|
+
#ifdef Py_GIL_DISABLED
|
|
3642
|
+
if (threads > 1 && whole->len >= (size_t(1) << 17)) {
|
|
3643
|
+
ParallelRun run;
|
|
3644
|
+
run.whole = whole;
|
|
3645
|
+
run.fn = fn;
|
|
3646
|
+
run.bounds = chunk_bounds(whole, threads * 8);
|
|
3647
|
+
run.chunks.resize(run.bounds.size() - 1);
|
|
3648
|
+
// Starting a thread costs about as much as building 512 KB of documents.
|
|
3649
|
+
size_t nthreads = std::min({threads, run.chunks.size(), std::max<size_t>(whole->len >> 19, 1)});
|
|
3650
|
+
std::vector<std::thread> pool;
|
|
3651
|
+
PyInterpreterState *interp = PyInterpreterState_Get();
|
|
3652
|
+
for (size_t t = 1; t < nthreads; t++) {
|
|
3653
|
+
try {
|
|
3654
|
+
pool.emplace_back([&run, interp] {
|
|
3655
|
+
PyThreadState *ts = PyThreadState_New(interp);
|
|
3656
|
+
if (ts == nullptr) {
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
PyEval_RestoreThread(ts);
|
|
3660
|
+
run.work();
|
|
3661
|
+
PyThreadState_Clear(ts);
|
|
3662
|
+
PyThreadState_DeleteCurrent();
|
|
3663
|
+
});
|
|
3664
|
+
} catch (...) {
|
|
3665
|
+
break; // fewer threads
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
run.work(); // the calling thread too
|
|
3669
|
+
Py_BEGIN_ALLOW_THREADS
|
|
3670
|
+
for (std::thread &t : pool) {
|
|
3671
|
+
t.join();
|
|
3672
|
+
}
|
|
3673
|
+
Py_END_ALLOW_THREADS
|
|
3674
|
+
size_t f = run.first_failure.load();
|
|
3675
|
+
size_t total = 0;
|
|
3676
|
+
for (size_t i = 0; i < run.chunks.size() && i <= f; i++) {
|
|
3677
|
+
total += run.chunks[i].items ? size_t(PyList_GET_SIZE(run.chunks[i].items)) : 0;
|
|
3678
|
+
}
|
|
3679
|
+
PyObject *exc = nullptr;
|
|
3680
|
+
int rc = 0;
|
|
3681
|
+
if (total > 0) {
|
|
3682
|
+
Py_DECREF(result);
|
|
3683
|
+
result = PyList_New(Py_ssize_t(total));
|
|
3684
|
+
rc = result == nullptr ? -1 : 0;
|
|
3685
|
+
}
|
|
3686
|
+
size_t k = 0;
|
|
3687
|
+
for (size_t i = 0; i < run.chunks.size(); i++) {
|
|
3688
|
+
ParallelRun::Chunk &c = run.chunks[i];
|
|
3689
|
+
if (c.items != nullptr && i <= f && rc == 0) {
|
|
3690
|
+
for (Py_ssize_t j = 0; j < PyList_GET_SIZE(c.items); j++) {
|
|
3691
|
+
PyList_SET_ITEM(result, Py_ssize_t(k++), Py_NewRef(PyList_GET_ITEM(c.items, j)));
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
Py_XDECREF(c.items);
|
|
3695
|
+
if (i == f) {
|
|
3696
|
+
exc = c.exc;
|
|
3697
|
+
resume_at = c.resume_at;
|
|
3698
|
+
} else {
|
|
3699
|
+
Py_XDECREF(c.exc);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
if (rc < 0) {
|
|
3703
|
+
Py_XDECREF(exc);
|
|
3704
|
+
return nullptr;
|
|
3705
|
+
}
|
|
3706
|
+
if (exc != nullptr) { // fn raised: as it would have without threads
|
|
3707
|
+
Py_DECREF(result);
|
|
3708
|
+
PyErr_SetObject(reinterpret_cast<PyObject *>(Py_TYPE(exc)), exc);
|
|
3709
|
+
Py_DECREF(exc);
|
|
3710
|
+
return nullptr;
|
|
3711
|
+
}
|
|
3712
|
+
if (f == SIZE_MAX) {
|
|
3713
|
+
return result;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
#else
|
|
3717
|
+
(void)threads;
|
|
3718
|
+
#endif
|
|
3719
|
+
// One thread, from resume_at to the end.
|
|
3720
|
+
StreamObject *st = resume_at == 0 ? reinterpret_cast<StreamObject *>(
|
|
3721
|
+
Py_NewRef(reinterpret_cast<PyObject *>(whole)))
|
|
3722
|
+
: stream_over(whole, resume_at, whole->len);
|
|
3723
|
+
if (st == nullptr) {
|
|
3724
|
+
Py_DECREF(result);
|
|
3725
|
+
return nullptr;
|
|
3726
|
+
}
|
|
3727
|
+
for (;;) {
|
|
3728
|
+
PyObject *v = stream_next_locked(st);
|
|
3729
|
+
if (v == nullptr) {
|
|
3730
|
+
break;
|
|
3731
|
+
}
|
|
3732
|
+
if (fn != nullptr) {
|
|
3733
|
+
PyObject *r = PyObject_CallOneArg(fn, v);
|
|
3734
|
+
Py_DECREF(v);
|
|
3735
|
+
if (r == nullptr) {
|
|
3736
|
+
break;
|
|
3737
|
+
}
|
|
3738
|
+
v = r;
|
|
3739
|
+
}
|
|
3740
|
+
int rc = PyList_Append(result, v);
|
|
3741
|
+
Py_DECREF(v);
|
|
3742
|
+
if (rc < 0) {
|
|
3743
|
+
break;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
Py_DECREF(reinterpret_cast<PyObject *>(st));
|
|
3747
|
+
if (PyErr_Occurred()) {
|
|
3748
|
+
Py_DECREF(result);
|
|
3749
|
+
return nullptr;
|
|
3750
|
+
}
|
|
3751
|
+
return result;
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
// Removes the keyword `threads` from kwargs (into *threads: 0 when absent or
|
|
3755
|
+
// None, else at least 1); returns the remaining keywords (new reference).
|
|
3756
|
+
PyObject *take_threads(PyObject *kwargs, Py_ssize_t *threads, bool *given) {
|
|
3757
|
+
*threads = 0;
|
|
3758
|
+
*given = false;
|
|
3759
|
+
if (kwargs == nullptr) {
|
|
3760
|
+
return nullptr;
|
|
3761
|
+
}
|
|
3762
|
+
PyObject *rest = PyDict_Copy(kwargs);
|
|
3763
|
+
if (rest == nullptr) {
|
|
3764
|
+
return nullptr;
|
|
3765
|
+
}
|
|
3766
|
+
PyObject *t = PyDict_GetItemString(rest, "threads"); // borrowed
|
|
3767
|
+
if (t == nullptr) {
|
|
3768
|
+
return rest;
|
|
3769
|
+
}
|
|
3770
|
+
Py_INCREF(t);
|
|
3771
|
+
PyDict_DelItemString(rest, "threads");
|
|
3772
|
+
*given = true;
|
|
3773
|
+
if (t != Py_None) {
|
|
3774
|
+
Py_ssize_t n = PyNumber_AsSsize_t(t, PyExc_OverflowError);
|
|
3775
|
+
if (n == -1 && PyErr_Occurred()) {
|
|
3776
|
+
Py_DECREF(t);
|
|
3777
|
+
Py_DECREF(rest);
|
|
3778
|
+
return nullptr;
|
|
3779
|
+
}
|
|
3780
|
+
if (n < 0) {
|
|
3781
|
+
PyErr_SetString(PyExc_ValueError, "threads must be at least 0 (0: one per processor)");
|
|
3782
|
+
Py_DECREF(t);
|
|
3783
|
+
Py_DECREF(rest);
|
|
3784
|
+
return nullptr;
|
|
3785
|
+
}
|
|
3786
|
+
*threads = n == 0 ? Py_ssize_t(std::max(1u, std::thread::hardware_concurrency())) : n;
|
|
3787
|
+
} else {
|
|
3788
|
+
*threads = Py_ssize_t(std::max(1u, std::thread::hardware_concurrency()));
|
|
3789
|
+
}
|
|
3790
|
+
Py_DECREF(t);
|
|
3791
|
+
return rest;
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
// The stream for loads_many/parse_many/map_many; with threads, the list of
|
|
3795
|
+
// all results instead.
|
|
3796
|
+
PyObject *stream_or_list(PyObject *args, PyObject *kwargs, bool lazy, PyObject *fn,
|
|
3797
|
+
bool force_list) {
|
|
3798
|
+
Py_ssize_t threads;
|
|
3799
|
+
bool given;
|
|
3800
|
+
PyObject *rest = take_threads(kwargs, &threads, &given);
|
|
3801
|
+
if (rest == nullptr && PyErr_Occurred()) {
|
|
3802
|
+
return nullptr;
|
|
3803
|
+
}
|
|
3804
|
+
PyObject *self = make_stream(args, rest, lazy);
|
|
3805
|
+
Py_XDECREF(rest);
|
|
3806
|
+
if (self == nullptr) {
|
|
3807
|
+
return nullptr;
|
|
3808
|
+
}
|
|
3809
|
+
if (!given && !force_list) {
|
|
3810
|
+
return self;
|
|
3811
|
+
}
|
|
3812
|
+
StreamObject *whole = reinterpret_cast<StreamObject *>(self);
|
|
3813
|
+
using simdjson::stream_format;
|
|
3814
|
+
if (given && whole->format != stream_format::whitespace_delimited &&
|
|
3815
|
+
whole->format != stream_format::newline_delimited &&
|
|
3816
|
+
whole->format != stream_format::json_sequence) {
|
|
3817
|
+
Py_DECREF(self);
|
|
3818
|
+
PyErr_SetString(PyExc_ValueError,
|
|
3819
|
+
"threads requires the format 'whitespace', 'lines' or 'json_seq'");
|
|
3820
|
+
return nullptr;
|
|
3821
|
+
}
|
|
3822
|
+
PyObject *r = run_stream(whole, fn, given ? size_t(threads) : 1);
|
|
3823
|
+
Py_DECREF(self);
|
|
3824
|
+
return r;
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3454
3827
|
PyObject *loads_many(PyObject *, PyObject *args, PyObject *kwargs) {
|
|
3455
|
-
return
|
|
3828
|
+
return stream_or_list(args, kwargs, false, nullptr, false);
|
|
3456
3829
|
}
|
|
3457
3830
|
|
|
3458
3831
|
PyObject *parse_many(PyObject *, PyObject *args, PyObject *kwargs) {
|
|
3459
|
-
return
|
|
3832
|
+
return stream_or_list(args, kwargs, true, nullptr, false);
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
// map_many(fn, data, *, format, batch_size, threads, lazy=False): [fn(doc) for
|
|
3836
|
+
// doc in loads_many(data)] (parse_many with lazy=True), with fn called in the
|
|
3837
|
+
// threads.
|
|
3838
|
+
PyObject *map_many(PyObject *, PyObject *args, PyObject *kwargs) {
|
|
3839
|
+
if (PyTuple_GET_SIZE(args) < 1) {
|
|
3840
|
+
PyErr_SetString(PyExc_TypeError, "map_many() missing required argument 'fn'");
|
|
3841
|
+
return nullptr;
|
|
3842
|
+
}
|
|
3843
|
+
PyObject *fn = PyTuple_GET_ITEM(args, 0);
|
|
3844
|
+
if (!PyCallable_Check(fn)) {
|
|
3845
|
+
PyErr_SetString(PyExc_TypeError, "map_many() fn must be callable");
|
|
3846
|
+
return nullptr;
|
|
3847
|
+
}
|
|
3848
|
+
bool lazy = false;
|
|
3849
|
+
PyObject *rest = kwargs ? PyDict_Copy(kwargs) : nullptr;
|
|
3850
|
+
if (kwargs != nullptr && rest == nullptr) {
|
|
3851
|
+
return nullptr;
|
|
3852
|
+
}
|
|
3853
|
+
if (rest != nullptr) {
|
|
3854
|
+
PyObject *l = PyDict_GetItemString(rest, "lazy"); // borrowed
|
|
3855
|
+
if (l != nullptr) {
|
|
3856
|
+
int t = PyObject_IsTrue(l);
|
|
3857
|
+
if (t < 0 || PyDict_DelItemString(rest, "lazy") < 0) {
|
|
3858
|
+
Py_DECREF(rest);
|
|
3859
|
+
return nullptr;
|
|
3860
|
+
}
|
|
3861
|
+
lazy = t != 0;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
PyObject *tail = PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args));
|
|
3865
|
+
if (tail == nullptr) {
|
|
3866
|
+
Py_XDECREF(rest);
|
|
3867
|
+
return nullptr;
|
|
3868
|
+
}
|
|
3869
|
+
PyObject *r = stream_or_list(tail, rest, lazy, fn, true);
|
|
3870
|
+
Py_DECREF(tail);
|
|
3871
|
+
Py_XDECREF(rest);
|
|
3872
|
+
return r;
|
|
3460
3873
|
}
|
|
3461
3874
|
|
|
3462
3875
|
// Benchmarking helper: run simdjson without building Python objects.
|
|
@@ -3497,10 +3910,15 @@ PyMethodDef methods[] = {
|
|
|
3497
3910
|
{"parse_file", parse_file, METH_O, "Parse the JSON file at path lazily, like parse."},
|
|
3498
3911
|
{"loads_many", reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)(void)>(loads_many)),
|
|
3499
3912
|
METH_VARARGS | METH_KEYWORDS,
|
|
3500
|
-
"Iterate over the JSON documents of data (NDJSON, JSON Lines, ...), like loads
|
|
3913
|
+
"Iterate over the JSON documents of data (NDJSON, JSON Lines, ...), like loads; "
|
|
3914
|
+
"with threads=N, the list of the documents, built by N threads."},
|
|
3501
3915
|
{"parse_many", reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)(void)>(parse_many)),
|
|
3502
3916
|
METH_VARARGS | METH_KEYWORDS,
|
|
3503
3917
|
"Iterate over the JSON documents of data lazily, like parse."},
|
|
3918
|
+
{"map_many", reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)(void)>(map_many)),
|
|
3919
|
+
METH_VARARGS | METH_KEYWORDS,
|
|
3920
|
+
"map_many(fn, data, *, format, batch_size, threads, lazy): the list of fn(doc) for the "
|
|
3921
|
+
"documents of data, with fn called in the threads."},
|
|
3504
3922
|
{"parse", parse, METH_O,
|
|
3505
3923
|
"Parse JSON lazily: objects and arrays are returned as read-only views "
|
|
3506
3924
|
"(Object, Array) whose values are converted on access."},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastsimdjson
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Fast JSON parsing for Python, built on simdjson
|
|
5
5
|
Author-email: Daniel Lemire <daniel@lemire.me>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -197,6 +197,40 @@ returned, otherwise `JSONDecodeError` reports `json`'s message and the
|
|
|
197
197
|
position in the whole input. A truncated last document is an error. The
|
|
198
198
|
views returned by `parse_many` remain valid after the iterator moves on.
|
|
199
199
|
|
|
200
|
+
### Streams on several threads: `threads=N` and `map_many`
|
|
201
|
+
|
|
202
|
+
On free-threaded Python (3.14t and newer), a stream can be parsed by several
|
|
203
|
+
threads at once:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
docs = fastsimdjson.loads_many(data, threads=16) # a list, in order
|
|
207
|
+
views = fastsimdjson.parse_many(data, threads=16) # a list of views
|
|
208
|
+
# fn runs in the threads too, on each document; its results, in order:
|
|
209
|
+
scores = fastsimdjson.map_many(score, data, threads=16)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
With `threads`, `loads_many` and `parse_many` return a list of all the
|
|
213
|
+
documents instead of an iterator. `map_many(fn, data, **kw)` returns
|
|
214
|
+
`[fn(doc) for doc in loads_many(data, **kw)]` (`parse_many` with
|
|
215
|
+
`lazy=True`), with `fn` called in the threads. `threads=0` (or `None`) uses
|
|
216
|
+
one thread per processor; small inputs use fewer threads (about one per
|
|
217
|
+
512 KB). The input is cut into chunks at newlines (before each `\x1e` for
|
|
218
|
+
`json_seq`), so `threads` works with the formats `"whitespace"`, `"lines"`
|
|
219
|
+
and `"json_seq"`.
|
|
220
|
+
|
|
221
|
+
The results, and the exceptions, are those of one thread: when a chunk
|
|
222
|
+
contains an error or a document that only `json` accepts, the documents
|
|
223
|
+
before it are kept and the rest is parsed in the calling thread, as without
|
|
224
|
+
threads. An exception raised by `fn` is raised again, for the first document
|
|
225
|
+
where it happened; `fn` may then have been called on later documents, in
|
|
226
|
+
other threads, and it must be safe to call from several threads at once.
|
|
227
|
+
|
|
228
|
+
`map_many` scales best: each document is built, used and freed in the same
|
|
229
|
+
thread, and only `fn`'s results come back. `loads_many` must return every
|
|
230
|
+
document, and the Python objects of all of them, to the calling thread.
|
|
231
|
+
With the GIL (the default build of Python), the calling thread does all the
|
|
232
|
+
work: `threads` has no effect on speed.
|
|
233
|
+
|
|
200
234
|
### `release`
|
|
201
235
|
|
|
202
236
|
`release()` frees the simdjson parser and the string caches kept by the
|
|
@@ -433,6 +467,24 @@ msgspec's `decode_lines` and `loads_many` are on par (`loads_many` is about 2%
|
|
|
433
467
|
faster). `parse_many` only creates the views; reading values from them adds to
|
|
434
468
|
its time.
|
|
435
469
|
|
|
470
|
+
### Streams on several threads
|
|
471
|
+
|
|
472
|
+
300 MB of NDJSON (146,624 records: tweets, GitHub events, citm performances,
|
|
473
|
+
gsoc and update-center entries), free-threaded Python 3.14.6, collector
|
|
474
|
+
disabled (see below), Intel Xeon Gold 6548N (64 cores); GB/s:
|
|
475
|
+
|
|
476
|
+
| threads | 1 | 2 | 4 | 8 | 16 | 32 | 64 |
|
|
477
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
478
|
+
| `map_many(len, data, threads=N)` | 0.91 | 1.77 | 3.28 | 6.22 | 10.9 | 15.4 | **21.2** |
|
|
479
|
+
| `parse_many(data, threads=N)` | 1.82 | 3.06 | 4.80 | 6.90 | 10.2 | 12.3 | 16.4 |
|
|
480
|
+
| `loads_many(data, threads=N)` | 0.82 | 1.43 | 2.26 | 3.88 | 5.77 | 7.45 | 8.63 |
|
|
481
|
+
|
|
482
|
+
`map_many` is 23 times faster on 64 threads than on one. `loads_many` builds
|
|
483
|
+
every document for the calling thread and is 11 times faster. With the
|
|
484
|
+
collector enabled, Python collects after the call returns: it examines the
|
|
485
|
+
millions of objects just created, which takes about 0.1 s here, with or
|
|
486
|
+
without threads.
|
|
487
|
+
|
|
436
488
|
## Limitations
|
|
437
489
|
|
|
438
490
|
* The simdjson parser and the key and string caches are thread-local.
|
|
@@ -222,3 +222,69 @@ def test_fallback_needs_the_separator():
|
|
|
222
222
|
with pytest.raises(fastsimdjson.JSONDecodeError) as info:
|
|
223
223
|
list(fastsimdjson.loads_many("[1e400 2]", format="array"))
|
|
224
224
|
assert info.value.msg == "Expecting ',' delimiter"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# threads=N: the list of all documents, as without threads. Inputs over
|
|
228
|
+
# 128 KB, so that free-threaded builds use several threads.
|
|
229
|
+
|
|
230
|
+
def _outcome(fn):
|
|
231
|
+
try:
|
|
232
|
+
return ("ok", fn())
|
|
233
|
+
except Exception as e: # noqa: BLE001
|
|
234
|
+
return (type(e).__name__, str(e), getattr(e, "pos", None))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _threaded_input(seed, fmt):
|
|
238
|
+
r = random.Random(seed)
|
|
239
|
+
docs = []
|
|
240
|
+
for i in range(6000):
|
|
241
|
+
c = r.randrange(100)
|
|
242
|
+
if c == 0:
|
|
243
|
+
docs.append(b"[1e400]") # json only: inf
|
|
244
|
+
elif c == 1:
|
|
245
|
+
docs.append(b'{"big": %d}' % (10**30 + i))
|
|
246
|
+
elif c == 2 and fmt == "whitespace":
|
|
247
|
+
docs.append(b'{\n "pretty": [1,\n 2]\n}') # spans lines
|
|
248
|
+
else:
|
|
249
|
+
docs.append(json.dumps({"i": i, "s": "x" * r.randrange(60), "f": i / 7}).encode())
|
|
250
|
+
if seed % 3 == 1:
|
|
251
|
+
docs[r.randrange(len(docs))] = b'{"broken": [1, 2' # an error somewhere
|
|
252
|
+
if seed % 3 == 2:
|
|
253
|
+
docs[r.randrange(len(docs))] = b'"\\ud800"' # json only: lone surrogate
|
|
254
|
+
sep = b"\n\x1e" if fmt == "json_seq" else b"\n"
|
|
255
|
+
data = sep.join(docs)
|
|
256
|
+
return b"\x1e" + data if fmt == "json_seq" else data
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@pytest.mark.parametrize("fmt", ["whitespace", "lines", "json_seq"])
|
|
260
|
+
@pytest.mark.parametrize("seed", range(9))
|
|
261
|
+
def test_threads_match_one_thread(fmt, seed):
|
|
262
|
+
data = _threaded_input(seed, fmt)
|
|
263
|
+
expected = _outcome(lambda: list(fastsimdjson.loads_many(data, format=fmt)))
|
|
264
|
+
for threads in (None, 0, 1, 2, 7):
|
|
265
|
+
assert _outcome(lambda: fastsimdjson.loads_many(data, format=fmt, threads=threads)) == expected
|
|
266
|
+
lazy = _outcome(lambda: [to_py(v) for v in fastsimdjson.parse_many(data, format=fmt, threads=4)])
|
|
267
|
+
assert lazy == expected
|
|
268
|
+
mapped = _outcome(lambda: fastsimdjson.map_many(json.dumps, data, format=fmt, threads=4))
|
|
269
|
+
assert mapped == (_outcome(lambda: [json.dumps(d) for d in expected[1]]) if expected[0] == "ok" else expected)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_threads_api():
|
|
273
|
+
data = b"\n".join(b'{"n": %d}' % i for i in range(30000))
|
|
274
|
+
docs = fastsimdjson.loads_many(data, threads=4)
|
|
275
|
+
assert isinstance(docs, list) and docs == [{"n": i} for i in range(30000)]
|
|
276
|
+
assert fastsimdjson.map_many(lambda d: d["n"], data, threads=3) == list(range(30000))
|
|
277
|
+
assert fastsimdjson.map_many(lambda v: v["n"], data, threads=3, lazy=True) == list(range(30000))
|
|
278
|
+
assert fastsimdjson.map_many(len, b"") == []
|
|
279
|
+
with pytest.raises(ValueError):
|
|
280
|
+
fastsimdjson.loads_many(b"[1],[2]", format="comma", threads=2)
|
|
281
|
+
with pytest.raises(ValueError):
|
|
282
|
+
fastsimdjson.loads_many(data, threads=-1)
|
|
283
|
+
|
|
284
|
+
def boom(d):
|
|
285
|
+
if d["n"] == 20000:
|
|
286
|
+
raise KeyError("boom")
|
|
287
|
+
return d
|
|
288
|
+
|
|
289
|
+
with pytest.raises(KeyError, match="boom"):
|
|
290
|
+
fastsimdjson.map_many(boom, data, threads=4)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|