fastsimdjson 0.5.0__tar.gz → 0.6.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.5.0/src/fastsimdjson.egg-info → fastsimdjson-0.6.0}/PKG-INFO +14 -12
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/README.md +13 -11
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/pyproject.toml +1 -1
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/src/fastsimdjson.cpp +298 -45
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0/src/fastsimdjson.egg-info}/PKG-INFO +14 -12
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/tests/test_stream.py +93 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/MANIFEST.in +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/setup.cfg +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/setup.py +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/src/fastsimdjson.egg-info/SOURCES.txt +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/src/fastsimdjson.egg-info/dependency_links.txt +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/src/fastsimdjson.egg-info/requires.txt +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/src/fastsimdjson.egg-info/top_level.txt +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/tests/test_dumps.py +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/tests/test_files.py +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/tests/test_lazy.py +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/tests/test_loads.py +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/vendor/simdjson.cpp +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/vendor/simdjson.h +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/vendor/simdutf.cpp +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/vendor/simdutf.h +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/vendor/simdutf_c.h +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/vendor/zmij-LICENSE +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.0}/vendor/zmij.cc +0 -0
- {fastsimdjson-0.5.0 → fastsimdjson-0.6.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.6.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
|
|
@@ -225,8 +225,10 @@ threads. An exception raised by `fn` is raised again, for the first document
|
|
|
225
225
|
where it happened; `fn` may then have been called on later documents, in
|
|
226
226
|
other threads, and it must be safe to call from several threads at once.
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
The worker threads are started by the first call that needs them and kept
|
|
229
|
+
for later calls (they wait, detached from the interpreter, between calls,
|
|
230
|
+
and stop at exit). `map_many` scales best: each document is built, used and
|
|
231
|
+
freed in the same thread, and only `fn`'s results come back. `loads_many` must return every
|
|
230
232
|
document, and the Python objects of all of them, to the calling thread.
|
|
231
233
|
With the GIL (the default build of Python), the calling thread does all the
|
|
232
234
|
work: `threads` has no effect on speed.
|
|
@@ -475,15 +477,15 @@ disabled (see below), Intel Xeon Gold 6548N (64 cores); GB/s:
|
|
|
475
477
|
|
|
476
478
|
| threads | 1 | 2 | 4 | 8 | 16 | 32 | 64 |
|
|
477
479
|
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
478
|
-
| `map_many(len, data, threads=N)` |
|
|
479
|
-
| `parse_many(data, threads=N)` |
|
|
480
|
-
| `loads_many(data, threads=N)` | 0.
|
|
481
|
-
|
|
482
|
-
`map_many` is
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
without threads.
|
|
480
|
+
| `map_many(len, data, threads=N)` | 1.03 | 1.78 | 3.66 | 6.82 | 13.0 | 23.7 | **28.8** |
|
|
481
|
+
| `parse_many(data, threads=N)` | 2.00 | 2.84 | 5.28 | 9.26 | 16.1 | 22.7 | 25.8 |
|
|
482
|
+
| `loads_many(data, threads=N)` | 0.90 | 1.44 | 2.79 | 4.99 | 8.98 | 13.3 | 15.4 |
|
|
483
|
+
|
|
484
|
+
`map_many` is 28 times faster on 64 threads than on one (up to 38 GB/s when
|
|
485
|
+
nothing else runs in the process). `loads_many` builds every document for
|
|
486
|
+
the calling thread and is 17 times faster. With the collector enabled,
|
|
487
|
+
Python collects after the call returns: it examines the millions of objects
|
|
488
|
+
just created, which takes about 0.1 s here, with or without threads.
|
|
487
489
|
|
|
488
490
|
## Limitations
|
|
489
491
|
|
|
@@ -194,8 +194,10 @@ threads. An exception raised by `fn` is raised again, for the first document
|
|
|
194
194
|
where it happened; `fn` may then have been called on later documents, in
|
|
195
195
|
other threads, and it must be safe to call from several threads at once.
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
The worker threads are started by the first call that needs them and kept
|
|
198
|
+
for later calls (they wait, detached from the interpreter, between calls,
|
|
199
|
+
and stop at exit). `map_many` scales best: each document is built, used and
|
|
200
|
+
freed in the same thread, and only `fn`'s results come back. `loads_many` must return every
|
|
199
201
|
document, and the Python objects of all of them, to the calling thread.
|
|
200
202
|
With the GIL (the default build of Python), the calling thread does all the
|
|
201
203
|
work: `threads` has no effect on speed.
|
|
@@ -444,15 +446,15 @@ disabled (see below), Intel Xeon Gold 6548N (64 cores); GB/s:
|
|
|
444
446
|
|
|
445
447
|
| threads | 1 | 2 | 4 | 8 | 16 | 32 | 64 |
|
|
446
448
|
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
447
|
-
| `map_many(len, data, threads=N)` |
|
|
448
|
-
| `parse_many(data, threads=N)` |
|
|
449
|
-
| `loads_many(data, threads=N)` | 0.
|
|
450
|
-
|
|
451
|
-
`map_many` is
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
without threads.
|
|
449
|
+
| `map_many(len, data, threads=N)` | 1.03 | 1.78 | 3.66 | 6.82 | 13.0 | 23.7 | **28.8** |
|
|
450
|
+
| `parse_many(data, threads=N)` | 2.00 | 2.84 | 5.28 | 9.26 | 16.1 | 22.7 | 25.8 |
|
|
451
|
+
| `loads_many(data, threads=N)` | 0.90 | 1.44 | 2.79 | 4.99 | 8.98 | 13.3 | 15.4 |
|
|
452
|
+
|
|
453
|
+
`map_many` is 28 times faster on 64 threads than on one (up to 38 GB/s when
|
|
454
|
+
nothing else runs in the process). `loads_many` builds every document for
|
|
455
|
+
the calling thread and is 17 times faster. With the collector enabled,
|
|
456
|
+
Python collects after the call returns: it examines the millions of objects
|
|
457
|
+
just created, which takes about 0.1 s here, with or without threads.
|
|
456
458
|
|
|
457
459
|
## Limitations
|
|
458
460
|
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
#include <cstdlib>
|
|
11
11
|
#include <cstring>
|
|
12
12
|
#include <new>
|
|
13
|
+
#include <condition_variable>
|
|
14
|
+
#include <deque>
|
|
15
|
+
#include <mutex>
|
|
13
16
|
#include <string>
|
|
14
17
|
#include <thread>
|
|
15
18
|
#include <vector>
|
|
@@ -20,6 +23,7 @@
|
|
|
20
23
|
#define WIN32_LEAN_AND_MEAN
|
|
21
24
|
#include <windows.h>
|
|
22
25
|
#else
|
|
26
|
+
#include <pthread.h>
|
|
23
27
|
#include <unistd.h>
|
|
24
28
|
#endif
|
|
25
29
|
|
|
@@ -68,6 +72,24 @@ inline int clz64(uint64_t x) { return __builtin_clzll(x); }
|
|
|
68
72
|
extern "C" int _PyDict_SetItem_KnownHash(PyObject *mp, PyObject *key,
|
|
69
73
|
PyObject *item, Py_hash_t hash);
|
|
70
74
|
#endif
|
|
75
|
+
#if defined(Py_GIL_DISABLED) && PY_VERSION_HEX >= 0x030E0000
|
|
76
|
+
extern "C" void _Py_SetImmortal(PyObject *op); // exported, not declared
|
|
77
|
+
#endif
|
|
78
|
+
|
|
79
|
+
#if defined(Py_GIL_DISABLED) && PY_VERSION_HEX >= 0x030D0000
|
|
80
|
+
// Without the GIL, _PyDict_SetItem_KnownHash locks the dict for each item.
|
|
81
|
+
// The dicts that loads builds are new and seen by no other thread until
|
|
82
|
+
// they are returned: they are filled without the lock.
|
|
83
|
+
extern "C" int _PyDict_SetItem_KnownHash_LockHeld(PyDictObject *mp, PyObject *key,
|
|
84
|
+
PyObject *item, Py_hash_t hash);
|
|
85
|
+
inline int new_dict_set_item(PyObject *mp, PyObject *key, PyObject *item, Py_hash_t hash) {
|
|
86
|
+
return _PyDict_SetItem_KnownHash_LockHeld(reinterpret_cast<PyDictObject *>(mp), key, item, hash);
|
|
87
|
+
}
|
|
88
|
+
#else
|
|
89
|
+
inline int new_dict_set_item(PyObject *mp, PyObject *key, PyObject *item, Py_hash_t hash) {
|
|
90
|
+
return _PyDict_SetItem_KnownHash(mp, key, item, hash);
|
|
91
|
+
}
|
|
92
|
+
#endif
|
|
71
93
|
|
|
72
94
|
#ifdef _MSC_VER
|
|
73
95
|
#define FSJ_ALWAYS_INLINE __forceinline
|
|
@@ -149,6 +171,8 @@ struct ThreadParser {
|
|
|
149
171
|
KeyCacheEntry value_cache[KEY_CACHE_SIZE]{};
|
|
150
172
|
EncodedKey *encoded_keys = nullptr; // allocated on first use by dumpb
|
|
151
173
|
bool cleanup_registered = false; // see register_thread_cleanup
|
|
174
|
+
parser *chunk_parser = nullptr; // for the chunks of parallel streams
|
|
175
|
+
bool chunk_parser_busy = false;
|
|
152
176
|
void clear_encoded_keys() {
|
|
153
177
|
if (encoded_keys != nullptr) {
|
|
154
178
|
for (size_t i = 0; i < ENCODED_KEY_CACHE_SIZE; i++) {
|
|
@@ -161,6 +185,10 @@ struct ThreadParser {
|
|
|
161
185
|
~ThreadParser() {
|
|
162
186
|
delete ptr;
|
|
163
187
|
ptr = nullptr;
|
|
188
|
+
if (!chunk_parser_busy) {
|
|
189
|
+
delete chunk_parser;
|
|
190
|
+
chunk_parser = nullptr;
|
|
191
|
+
}
|
|
164
192
|
delete spare;
|
|
165
193
|
spare = nullptr;
|
|
166
194
|
if (caches_can_decref()) {
|
|
@@ -176,6 +204,10 @@ thread_local ThreadParser g_thread_parser;
|
|
|
176
204
|
void release_thread_state() {
|
|
177
205
|
delete g_thread_parser.ptr;
|
|
178
206
|
g_thread_parser.ptr = nullptr;
|
|
207
|
+
if (!g_thread_parser.chunk_parser_busy) {
|
|
208
|
+
delete g_thread_parser.chunk_parser;
|
|
209
|
+
g_thread_parser.chunk_parser = nullptr;
|
|
210
|
+
}
|
|
179
211
|
delete g_thread_parser.spare;
|
|
180
212
|
g_thread_parser.spare = nullptr;
|
|
181
213
|
clear_cache(g_thread_parser.key_cache);
|
|
@@ -547,8 +579,7 @@ struct Builder {
|
|
|
547
579
|
Py_DECREF(dict);
|
|
548
580
|
return nullptr;
|
|
549
581
|
}
|
|
550
|
-
int rc =
|
|
551
|
-
dict, key, v, reinterpret_cast<PyASCIIObject *>(key)->hash);
|
|
582
|
+
int rc = new_dict_set_item(dict, key, v, reinterpret_cast<PyASCIIObject *>(key)->hash);
|
|
552
583
|
Py_DECREF(key);
|
|
553
584
|
Py_DECREF(v);
|
|
554
585
|
if (rc < 0) {
|
|
@@ -761,6 +792,7 @@ PyTypeObject *IterType = nullptr;
|
|
|
761
792
|
struct DocumentObject {
|
|
762
793
|
PyObject_HEAD
|
|
763
794
|
document *doc;
|
|
795
|
+
bool copied; // made by copy_document, freed by free_copy
|
|
764
796
|
};
|
|
765
797
|
|
|
766
798
|
// fastsimdjson.Object and fastsimdjson.Array.
|
|
@@ -972,9 +1004,16 @@ PyObject *at_pointer(DocumentObject *owner, size_t i, PyObject *arg) {
|
|
|
972
1004
|
|
|
973
1005
|
// --- Document ---------------------------------------------------------------
|
|
974
1006
|
|
|
1007
|
+
void free_copy(document *d);
|
|
1008
|
+
|
|
975
1009
|
void document_dealloc(PyObject *self) {
|
|
976
1010
|
PyTypeObject *tp = Py_TYPE(self);
|
|
977
|
-
|
|
1011
|
+
DocumentObject *owner = reinterpret_cast<DocumentObject *>(self);
|
|
1012
|
+
if (owner->copied) {
|
|
1013
|
+
free_copy(owner->doc);
|
|
1014
|
+
} else {
|
|
1015
|
+
give_back(owner->doc);
|
|
1016
|
+
}
|
|
978
1017
|
PyObject_Free(self);
|
|
979
1018
|
Py_DECREF(tp);
|
|
980
1019
|
}
|
|
@@ -1270,14 +1309,20 @@ PyType_Spec iter_spec = {
|
|
|
1270
1309
|
|
|
1271
1310
|
// --- parse() ----------------------------------------------------------------
|
|
1272
1311
|
|
|
1273
|
-
// The root of document d, which the returned view takes over
|
|
1274
|
-
|
|
1312
|
+
// The root of document d, which the returned view takes over (d is a copy
|
|
1313
|
+
// from copy_document when `copied`).
|
|
1314
|
+
PyObject *view_of(document *d, bool copied = false) {
|
|
1275
1315
|
DocumentObject *owner = PyObject_New(DocumentObject, DocumentType);
|
|
1276
1316
|
if (owner == nullptr) {
|
|
1277
|
-
|
|
1317
|
+
if (copied) {
|
|
1318
|
+
free_copy(d);
|
|
1319
|
+
} else {
|
|
1320
|
+
give_back(d);
|
|
1321
|
+
}
|
|
1278
1322
|
return nullptr;
|
|
1279
1323
|
}
|
|
1280
1324
|
owner->doc = d;
|
|
1325
|
+
owner->copied = copied;
|
|
1281
1326
|
PyObject *root = value_at(owner, 1); // skip the root entry
|
|
1282
1327
|
Py_DECREF(owner);
|
|
1283
1328
|
return root;
|
|
@@ -3056,6 +3101,7 @@ struct StreamObject {
|
|
|
3056
3101
|
size_t text_len; // bytes of buf that `text` covers (len, or more for a chunk)
|
|
3057
3102
|
bool no_fallback; // a chunk of a parallel run: do not call json (see run_chunk)
|
|
3058
3103
|
bool need_fallback; // set instead of calling json, with no_fallback
|
|
3104
|
+
bool borrowed_parser; // p is this thread's chunk parser (not deleted)
|
|
3059
3105
|
parser *p;
|
|
3060
3106
|
document_stream *stream;
|
|
3061
3107
|
document_stream::iterator it;
|
|
@@ -3171,7 +3217,13 @@ bool stream_start(StreamObject *s, size_t at) {
|
|
|
3171
3217
|
return true;
|
|
3172
3218
|
}
|
|
3173
3219
|
|
|
3174
|
-
// A copy of the parser's current document,
|
|
3220
|
+
// A copy of the parser's current document, in one block from PyMem_Malloc
|
|
3221
|
+
// (on an attached thread): the document, then its tape, then its strings.
|
|
3222
|
+
// Streams on several threads make many of them at once, and the calling
|
|
3223
|
+
// thread frees them later: with glibc's malloc, the threads' heaps grew and
|
|
3224
|
+
// shrank at every call, and the system calls doing it are serialized.
|
|
3225
|
+
// PyMem_Malloc is mimalloc on free-threaded builds, without this problem.
|
|
3226
|
+
// The document's buffers are not its own: free_copy releases them first.
|
|
3175
3227
|
document *copy_document(const document &src) {
|
|
3176
3228
|
const uint64_t *tape = src.tape.get();
|
|
3177
3229
|
// The root entry holds the index just past the last tape entry.
|
|
@@ -3188,27 +3240,37 @@ document *copy_document(const document &src) {
|
|
|
3188
3240
|
i++;
|
|
3189
3241
|
}
|
|
3190
3242
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
d->string_buf.reset(new (std::nothrow) uint8_t[strings + simdjson::SIMDJSON_PADDING]());
|
|
3197
|
-
if (!d->tape || !d->string_buf) {
|
|
3198
|
-
delete d;
|
|
3243
|
+
size_t head = (sizeof(document) + 63) & ~size_t(63);
|
|
3244
|
+
size_t tape_bytes = n * sizeof(uint64_t);
|
|
3245
|
+
char *block = static_cast<char *>(
|
|
3246
|
+
PyMem_Malloc(head + tape_bytes + strings + simdjson::SIMDJSON_PADDING));
|
|
3247
|
+
if (block == nullptr) {
|
|
3199
3248
|
return nullptr;
|
|
3200
3249
|
}
|
|
3201
|
-
|
|
3202
|
-
|
|
3250
|
+
document *d = new (block) document();
|
|
3251
|
+
uint64_t *t = reinterpret_cast<uint64_t *>(block + head);
|
|
3252
|
+
uint8_t *str = reinterpret_cast<uint8_t *>(block + head + tape_bytes);
|
|
3253
|
+
memcpy(t, tape, tape_bytes);
|
|
3254
|
+
memcpy(str, src.string_buf.get(), strings);
|
|
3255
|
+
memset(str + strings, 0, simdjson::SIMDJSON_PADDING);
|
|
3256
|
+
d->tape.reset(t);
|
|
3257
|
+
d->string_buf.reset(str);
|
|
3203
3258
|
return d;
|
|
3204
3259
|
}
|
|
3205
3260
|
|
|
3261
|
+
void free_copy(document *d) {
|
|
3262
|
+
(void)d->tape.release(); // in the block, not allocated with new[]
|
|
3263
|
+
(void)d->string_buf.release();
|
|
3264
|
+
d->~document();
|
|
3265
|
+
PyMem_Free(d);
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3206
3268
|
PyObject *stream_value(StreamObject *s) {
|
|
3207
3269
|
if (!s->lazy) {
|
|
3208
3270
|
return build_with_gc_paused(s->p->doc, 1);
|
|
3209
3271
|
}
|
|
3210
3272
|
document *d = copy_document(s->p->doc);
|
|
3211
|
-
return d == nullptr ? PyErr_NoMemory() : view_of(d);
|
|
3273
|
+
return d == nullptr ? PyErr_NoMemory() : view_of(d, true);
|
|
3212
3274
|
}
|
|
3213
3275
|
|
|
3214
3276
|
// simdjson rejected the document after last_end. json decides, as in loads:
|
|
@@ -3320,7 +3382,11 @@ void stream_dealloc(PyObject *self) {
|
|
|
3320
3382
|
PyTypeObject *tp = Py_TYPE(self);
|
|
3321
3383
|
s->it.~iterator();
|
|
3322
3384
|
delete s->stream;
|
|
3323
|
-
|
|
3385
|
+
if (s->borrowed_parser) {
|
|
3386
|
+
g_thread_parser.chunk_parser_busy = false;
|
|
3387
|
+
} else {
|
|
3388
|
+
delete s->p;
|
|
3389
|
+
}
|
|
3324
3390
|
delete s->copy;
|
|
3325
3391
|
Py_XDECREF(s->source);
|
|
3326
3392
|
Py_XDECREF(s->text);
|
|
@@ -3407,7 +3473,7 @@ PyObject *make_stream(PyObject *args, PyObject *kwargs, bool lazy) {
|
|
|
3407
3473
|
s->active = false;
|
|
3408
3474
|
s->base = 0;
|
|
3409
3475
|
s->last_end = 0;
|
|
3410
|
-
s->no_fallback = s->need_fallback = false;
|
|
3476
|
+
s->no_fallback = s->need_fallback = s->borrowed_parser = false;
|
|
3411
3477
|
PyObject *self = reinterpret_cast<PyObject *>(s);
|
|
3412
3478
|
if (must_copy) {
|
|
3413
3479
|
PyObject *b = PyBytes_FromObject(data);
|
|
@@ -3481,8 +3547,9 @@ PyObject *make_stream(PyObject *args, PyObject *kwargs, bool lazy) {
|
|
|
3481
3547
|
// called on documents after that point, in other threads.)
|
|
3482
3548
|
// ---------------------------------------------------------------------------
|
|
3483
3549
|
|
|
3484
|
-
// A stream over [from, to) of the input of `whole`, sharing its buffer.
|
|
3485
|
-
|
|
3550
|
+
// A stream over [from, to) of the input of `whole`, sharing its buffer. A
|
|
3551
|
+
// chunk of a parallel run uses this thread's chunk parser when it is free.
|
|
3552
|
+
StreamObject *stream_over(StreamObject *whole, size_t from, size_t to, bool chunk = false) {
|
|
3486
3553
|
StreamObject *s = PyObject_New(StreamObject, StreamType);
|
|
3487
3554
|
if (s == nullptr) {
|
|
3488
3555
|
return nullptr;
|
|
@@ -3506,14 +3573,29 @@ StreamObject *stream_over(StreamObject *whole, size_t from, size_t to) {
|
|
|
3506
3573
|
s->prefix = whole->prefix;
|
|
3507
3574
|
s->open = whole->open;
|
|
3508
3575
|
s->close = whole->close;
|
|
3509
|
-
s->no_fallback = s->need_fallback = false;
|
|
3510
|
-
|
|
3576
|
+
s->no_fallback = s->need_fallback = s->borrowed_parser = false;
|
|
3577
|
+
if (chunk && !g_thread_parser.chunk_parser_busy) {
|
|
3578
|
+
if (g_thread_parser.chunk_parser == nullptr) {
|
|
3579
|
+
register_thread_cleanup();
|
|
3580
|
+
g_thread_parser.chunk_parser = new (std::nothrow) parser();
|
|
3581
|
+
}
|
|
3582
|
+
if (g_thread_parser.chunk_parser != nullptr) {
|
|
3583
|
+
s->p = g_thread_parser.chunk_parser;
|
|
3584
|
+
s->borrowed_parser = g_thread_parser.chunk_parser_busy = true;
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
if (s->p == nullptr) {
|
|
3588
|
+
s->p = new (std::nothrow) parser();
|
|
3589
|
+
}
|
|
3511
3590
|
if (s->p == nullptr) {
|
|
3512
3591
|
Py_DECREF(reinterpret_cast<PyObject *>(s));
|
|
3513
3592
|
PyErr_NoMemory();
|
|
3514
3593
|
return nullptr;
|
|
3515
3594
|
}
|
|
3516
3595
|
s->p->number_as_string(true);
|
|
3596
|
+
// simdjson's streams run stage 1 of the next batch in a thread of their
|
|
3597
|
+
// own; the threads of a parallel run already use all the cores.
|
|
3598
|
+
s->p->threaded = !chunk;
|
|
3517
3599
|
return s;
|
|
3518
3600
|
}
|
|
3519
3601
|
|
|
@@ -3530,6 +3612,7 @@ struct ParallelRun {
|
|
|
3530
3612
|
std::vector<Chunk> chunks;
|
|
3531
3613
|
std::atomic<size_t> next{0};
|
|
3532
3614
|
std::atomic<size_t> first_failure{SIZE_MAX};
|
|
3615
|
+
size_t active = 0; // pool workers in work(), guarded by the pool's mutex
|
|
3533
3616
|
|
|
3534
3617
|
void note_failure(size_t i) {
|
|
3535
3618
|
size_t f = first_failure.load();
|
|
@@ -3541,7 +3624,7 @@ struct ParallelRun {
|
|
|
3541
3624
|
void run_chunk(size_t i) {
|
|
3542
3625
|
Chunk &c = chunks[i];
|
|
3543
3626
|
c.resume_at = bounds[i];
|
|
3544
|
-
StreamObject *st = stream_over(whole, bounds[i], bounds[i + 1]);
|
|
3627
|
+
StreamObject *st = stream_over(whole, bounds[i], bounds[i + 1], true);
|
|
3545
3628
|
c.items = st ? PyList_New(0) : nullptr;
|
|
3546
3629
|
if (c.items == nullptr) {
|
|
3547
3630
|
PyErr_Clear(); // the one-thread run will report it
|
|
@@ -3609,6 +3692,147 @@ struct ParallelRun {
|
|
|
3609
3692
|
}
|
|
3610
3693
|
};
|
|
3611
3694
|
|
|
3695
|
+
#ifdef Py_GIL_DISABLED
|
|
3696
|
+
// Worker threads for the parallel streams, started once and kept: starting
|
|
3697
|
+
// and ending threads (with their Python thread states) cost about as much
|
|
3698
|
+
// as the parsing on many cores. Idle workers are detached from Python and
|
|
3699
|
+
// wait for tasks: a task is a run to help with. A caller does the work of
|
|
3700
|
+
// its run too; at the end, it takes back the tasks that no worker started
|
|
3701
|
+
// and waits only for the workers that did start, so concurrent and nested
|
|
3702
|
+
// runs cannot deadlock.
|
|
3703
|
+
struct WorkerPool {
|
|
3704
|
+
std::mutex mu;
|
|
3705
|
+
std::condition_variable wake; // tasks, or stopping
|
|
3706
|
+
std::condition_variable done; // a worker finished a task, or exited
|
|
3707
|
+
std::deque<ParallelRun *> tasks;
|
|
3708
|
+
PyInterpreterState *interp = nullptr;
|
|
3709
|
+
size_t workers = 0;
|
|
3710
|
+
size_t ready = 0; // workers with a thread state, waiting for tasks
|
|
3711
|
+
size_t exited = 0;
|
|
3712
|
+
bool stopping = false;
|
|
3713
|
+
};
|
|
3714
|
+
WorkerPool *g_pool = nullptr; // never freed: its threads may outlive anything
|
|
3715
|
+
std::mutex g_pool_mu;
|
|
3716
|
+
|
|
3717
|
+
void pool_worker(WorkerPool *pool) {
|
|
3718
|
+
PyThreadState *ts = PyThreadState_New(pool->interp);
|
|
3719
|
+
{
|
|
3720
|
+
std::lock_guard<std::mutex> lk(pool->mu);
|
|
3721
|
+
pool->ready++;
|
|
3722
|
+
}
|
|
3723
|
+
pool->done.notify_all();
|
|
3724
|
+
for (;;) {
|
|
3725
|
+
ParallelRun *run;
|
|
3726
|
+
{
|
|
3727
|
+
std::unique_lock<std::mutex> lk(pool->mu);
|
|
3728
|
+
pool->wake.wait(lk, [pool] { return pool->stopping || !pool->tasks.empty(); });
|
|
3729
|
+
if (pool->tasks.empty()) {
|
|
3730
|
+
break; // stopping
|
|
3731
|
+
}
|
|
3732
|
+
run = pool->tasks.front();
|
|
3733
|
+
pool->tasks.pop_front();
|
|
3734
|
+
run->active++;
|
|
3735
|
+
}
|
|
3736
|
+
if (ts != nullptr) {
|
|
3737
|
+
PyEval_RestoreThread(ts);
|
|
3738
|
+
run->work();
|
|
3739
|
+
PyEval_SaveThread();
|
|
3740
|
+
}
|
|
3741
|
+
{
|
|
3742
|
+
std::lock_guard<std::mutex> lk(pool->mu);
|
|
3743
|
+
run->active--;
|
|
3744
|
+
}
|
|
3745
|
+
pool->done.notify_all();
|
|
3746
|
+
}
|
|
3747
|
+
if (ts != nullptr) {
|
|
3748
|
+
PyEval_RestoreThread(ts);
|
|
3749
|
+
PyThreadState_Clear(ts);
|
|
3750
|
+
PyThreadState_DeleteCurrent();
|
|
3751
|
+
}
|
|
3752
|
+
{
|
|
3753
|
+
std::lock_guard<std::mutex> lk(pool->mu);
|
|
3754
|
+
pool->exited++;
|
|
3755
|
+
}
|
|
3756
|
+
pool->done.notify_all();
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
// The pool, with at least n workers (fewer if threads cannot be started).
|
|
3760
|
+
// New workers create their thread states before any work is handed out:
|
|
3761
|
+
// CPython may stop the world to make room for a thread state, which must
|
|
3762
|
+
// then wait for every attached thread, including busy workers. So the
|
|
3763
|
+
// caller waits, detached, for the new workers to be ready. (No lock is held
|
|
3764
|
+
// while a thread waits for others.)
|
|
3765
|
+
WorkerPool *get_pool(size_t n) {
|
|
3766
|
+
WorkerPool *pool;
|
|
3767
|
+
size_t target;
|
|
3768
|
+
{
|
|
3769
|
+
std::lock_guard<std::mutex> g(g_pool_mu);
|
|
3770
|
+
PyInterpreterState *interp = PyInterpreterState_Get();
|
|
3771
|
+
if (g_pool == nullptr) {
|
|
3772
|
+
g_pool = new (std::nothrow) WorkerPool();
|
|
3773
|
+
if (g_pool == nullptr) {
|
|
3774
|
+
return nullptr;
|
|
3775
|
+
}
|
|
3776
|
+
g_pool->interp = interp;
|
|
3777
|
+
}
|
|
3778
|
+
pool = g_pool;
|
|
3779
|
+
if (pool->interp != interp || pool->stopping) {
|
|
3780
|
+
return nullptr; // another interpreter, or exiting: no helpers
|
|
3781
|
+
}
|
|
3782
|
+
while (pool->workers < std::min<size_t>(n, 1024)) {
|
|
3783
|
+
try {
|
|
3784
|
+
std::thread(pool_worker, pool).detach();
|
|
3785
|
+
} catch (...) {
|
|
3786
|
+
break;
|
|
3787
|
+
}
|
|
3788
|
+
std::lock_guard<std::mutex> lk(pool->mu);
|
|
3789
|
+
pool->workers++;
|
|
3790
|
+
}
|
|
3791
|
+
target = pool->workers;
|
|
3792
|
+
}
|
|
3793
|
+
bool waiting;
|
|
3794
|
+
{
|
|
3795
|
+
std::lock_guard<std::mutex> lk(pool->mu);
|
|
3796
|
+
waiting = pool->ready < target;
|
|
3797
|
+
}
|
|
3798
|
+
if (waiting) {
|
|
3799
|
+
Py_BEGIN_ALLOW_THREADS
|
|
3800
|
+
std::unique_lock<std::mutex> lk(pool->mu);
|
|
3801
|
+
pool->done.wait(lk, [pool, target] { return pool->ready >= target; });
|
|
3802
|
+
Py_END_ALLOW_THREADS
|
|
3803
|
+
}
|
|
3804
|
+
return pool;
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
// atexit: the workers delete their thread states while the interpreter is
|
|
3808
|
+
// still alive, then exit.
|
|
3809
|
+
PyObject *stop_pool(PyObject *, PyObject *) {
|
|
3810
|
+
WorkerPool *pool;
|
|
3811
|
+
{
|
|
3812
|
+
std::lock_guard<std::mutex> g(g_pool_mu);
|
|
3813
|
+
pool = g_pool;
|
|
3814
|
+
}
|
|
3815
|
+
if (pool != nullptr) {
|
|
3816
|
+
Py_BEGIN_ALLOW_THREADS
|
|
3817
|
+
std::unique_lock<std::mutex> lk(pool->mu);
|
|
3818
|
+
pool->stopping = true;
|
|
3819
|
+
pool->wake.notify_all();
|
|
3820
|
+
pool->done.wait(lk, [pool] { return pool->exited == pool->workers; });
|
|
3821
|
+
Py_END_ALLOW_THREADS
|
|
3822
|
+
}
|
|
3823
|
+
Py_RETURN_NONE;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
#ifndef _WIN32
|
|
3827
|
+
// A forked child has none of the workers: forget the pool (and its mutexes,
|
|
3828
|
+
// which another thread may have held during the fork).
|
|
3829
|
+
void pool_after_fork() {
|
|
3830
|
+
g_pool = nullptr;
|
|
3831
|
+
new (&g_pool_mu) std::mutex();
|
|
3832
|
+
}
|
|
3833
|
+
#endif
|
|
3834
|
+
#endif
|
|
3835
|
+
|
|
3612
3836
|
// Chunk boundaries: about `n` chunks, each ending just after a newline (or
|
|
3613
3837
|
// just before a record separator, for json_seq).
|
|
3614
3838
|
std::vector<size_t> chunk_bounds(const StreamObject *w, size_t n) {
|
|
@@ -3647,30 +3871,26 @@ PyObject *run_stream(StreamObject *whole, PyObject *fn, size_t threads) {
|
|
|
3647
3871
|
run.chunks.resize(run.bounds.size() - 1);
|
|
3648
3872
|
// Starting a thread costs about as much as building 512 KB of documents.
|
|
3649
3873
|
size_t nthreads = std::min({threads, run.chunks.size(), std::max<size_t>(whole->len >> 19, 1)});
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
return;
|
|
3658
|
-
}
|
|
3659
|
-
PyEval_RestoreThread(ts);
|
|
3660
|
-
run.work();
|
|
3661
|
-
PyThreadState_Clear(ts);
|
|
3662
|
-
PyThreadState_DeleteCurrent();
|
|
3663
|
-
});
|
|
3664
|
-
} catch (...) {
|
|
3665
|
-
break; // fewer threads
|
|
3874
|
+
WorkerPool *pool = nthreads > 1 ? get_pool(nthreads - 1) : nullptr;
|
|
3875
|
+
if (pool != nullptr) {
|
|
3876
|
+
{
|
|
3877
|
+
std::lock_guard<std::mutex> lk(pool->mu);
|
|
3878
|
+
for (size_t t = 1; t < nthreads; t++) {
|
|
3879
|
+
pool->tasks.push_back(&run);
|
|
3880
|
+
}
|
|
3666
3881
|
}
|
|
3882
|
+
pool->wake.notify_all();
|
|
3667
3883
|
}
|
|
3668
3884
|
run.work(); // the calling thread too
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3885
|
+
if (pool != nullptr) {
|
|
3886
|
+
Py_BEGIN_ALLOW_THREADS
|
|
3887
|
+
std::unique_lock<std::mutex> lk(pool->mu);
|
|
3888
|
+
for (auto it = pool->tasks.begin(); it != pool->tasks.end();) {
|
|
3889
|
+
it = *it == &run ? pool->tasks.erase(it) : it + 1;
|
|
3890
|
+
}
|
|
3891
|
+
pool->done.wait(lk, [&run] { return run.active == 0; });
|
|
3892
|
+
Py_END_ALLOW_THREADS
|
|
3672
3893
|
}
|
|
3673
|
-
Py_END_ALLOW_THREADS
|
|
3674
3894
|
size_t f = run.first_failure.load();
|
|
3675
3895
|
size_t total = 0;
|
|
3676
3896
|
for (size_t i = 0; i < run.chunks.size() && i <= f; i++) {
|
|
@@ -3915,6 +4135,9 @@ PyMethodDef methods[] = {
|
|
|
3915
4135
|
{"parse_many", reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)(void)>(parse_many)),
|
|
3916
4136
|
METH_VARARGS | METH_KEYWORDS,
|
|
3917
4137
|
"Iterate over the JSON documents of data lazily, like parse."},
|
|
4138
|
+
#ifdef Py_GIL_DISABLED
|
|
4139
|
+
{"_stop_pool", stop_pool, METH_NOARGS, "Stop the worker threads (at exit)."},
|
|
4140
|
+
#endif
|
|
3918
4141
|
{"map_many", reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)(void)>(map_many)),
|
|
3919
4142
|
METH_VARARGS | METH_KEYWORDS,
|
|
3920
4143
|
"map_many(fn, data, *, format, batch_size, threads, lazy): the list of fn(doc) for the "
|
|
@@ -3941,6 +4164,26 @@ int register_abc(const char *name, PyTypeObject *type) {
|
|
|
3941
4164
|
// Multi-phase init so the module can declare that it does not need the GIL.
|
|
3942
4165
|
// Exception types are process-global, so subinterpreters stay unsupported.
|
|
3943
4166
|
int exec_fastsimdjson(PyObject *module) {
|
|
4167
|
+
#ifdef Py_GIL_DISABLED
|
|
4168
|
+
{
|
|
4169
|
+
PyObject *atexit = PyImport_ImportModule("atexit");
|
|
4170
|
+
PyObject *stop = atexit ? PyObject_GetAttrString(module, "_stop_pool") : nullptr;
|
|
4171
|
+
PyObject *r = stop ? PyObject_CallMethod(atexit, "register", "O", stop) : nullptr;
|
|
4172
|
+
Py_XDECREF(r);
|
|
4173
|
+
Py_XDECREF(stop);
|
|
4174
|
+
Py_XDECREF(atexit);
|
|
4175
|
+
if (r == nullptr) {
|
|
4176
|
+
return -1;
|
|
4177
|
+
}
|
|
4178
|
+
#ifndef _WIN32
|
|
4179
|
+
static bool atfork_done = false;
|
|
4180
|
+
if (!atfork_done) {
|
|
4181
|
+
pthread_atfork(nullptr, nullptr, pool_after_fork);
|
|
4182
|
+
atfork_done = true;
|
|
4183
|
+
}
|
|
4184
|
+
#endif
|
|
4185
|
+
}
|
|
4186
|
+
#endif
|
|
3944
4187
|
#if FSJ_AVX512
|
|
3945
4188
|
has_avx512 = detect_avx512();
|
|
3946
4189
|
#endif
|
|
@@ -4019,6 +4262,16 @@ int exec_fastsimdjson(PyObject *module) {
|
|
|
4019
4262
|
if (*t.type == nullptr) {
|
|
4020
4263
|
return -1;
|
|
4021
4264
|
}
|
|
4265
|
+
#if defined(Py_GIL_DISABLED) && PY_VERSION_HEX >= 0x030E0000
|
|
4266
|
+
// Each instance holds a reference to its heap type, released in its
|
|
4267
|
+
// dealloc with Py_DECREF. CPython takes that reference with a
|
|
4268
|
+
// per-thread count, but without the GIL, Py_DECREF is an atomic
|
|
4269
|
+
// operation on the type, which all the threads making views contend
|
|
4270
|
+
// for (views from 64 threads ran about 100 times slower than from
|
|
4271
|
+
// one). Immortal types are not counted. The types are kept for the
|
|
4272
|
+
// life of the process anyway (no subinterpreters).
|
|
4273
|
+
_Py_SetImmortal(reinterpret_cast<PyObject *>(*t.type));
|
|
4274
|
+
#endif
|
|
4022
4275
|
}
|
|
4023
4276
|
}
|
|
4024
4277
|
// isinstance(x, Mapping) / isinstance(x, Sequence) hold for the views.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastsimdjson
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -225,8 +225,10 @@ threads. An exception raised by `fn` is raised again, for the first document
|
|
|
225
225
|
where it happened; `fn` may then have been called on later documents, in
|
|
226
226
|
other threads, and it must be safe to call from several threads at once.
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
The worker threads are started by the first call that needs them and kept
|
|
229
|
+
for later calls (they wait, detached from the interpreter, between calls,
|
|
230
|
+
and stop at exit). `map_many` scales best: each document is built, used and
|
|
231
|
+
freed in the same thread, and only `fn`'s results come back. `loads_many` must return every
|
|
230
232
|
document, and the Python objects of all of them, to the calling thread.
|
|
231
233
|
With the GIL (the default build of Python), the calling thread does all the
|
|
232
234
|
work: `threads` has no effect on speed.
|
|
@@ -475,15 +477,15 @@ disabled (see below), Intel Xeon Gold 6548N (64 cores); GB/s:
|
|
|
475
477
|
|
|
476
478
|
| threads | 1 | 2 | 4 | 8 | 16 | 32 | 64 |
|
|
477
479
|
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
478
|
-
| `map_many(len, data, threads=N)` |
|
|
479
|
-
| `parse_many(data, threads=N)` |
|
|
480
|
-
| `loads_many(data, threads=N)` | 0.
|
|
481
|
-
|
|
482
|
-
`map_many` is
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
without threads.
|
|
480
|
+
| `map_many(len, data, threads=N)` | 1.03 | 1.78 | 3.66 | 6.82 | 13.0 | 23.7 | **28.8** |
|
|
481
|
+
| `parse_many(data, threads=N)` | 2.00 | 2.84 | 5.28 | 9.26 | 16.1 | 22.7 | 25.8 |
|
|
482
|
+
| `loads_many(data, threads=N)` | 0.90 | 1.44 | 2.79 | 4.99 | 8.98 | 13.3 | 15.4 |
|
|
483
|
+
|
|
484
|
+
`map_many` is 28 times faster on 64 threads than on one (up to 38 GB/s when
|
|
485
|
+
nothing else runs in the process). `loads_many` builds every document for
|
|
486
|
+
the calling thread and is 17 times faster. With the collector enabled,
|
|
487
|
+
Python collects after the call returns: it examines the millions of objects
|
|
488
|
+
just created, which takes about 0.1 s here, with or without threads.
|
|
487
489
|
|
|
488
490
|
## Limitations
|
|
489
491
|
|
|
@@ -288,3 +288,96 @@ def test_threads_api():
|
|
|
288
288
|
|
|
289
289
|
with pytest.raises(KeyError, match="boom"):
|
|
290
290
|
fastsimdjson.map_many(boom, data, threads=4)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# The worker threads of threads=N persist between calls (free-threaded
|
|
294
|
+
# Python): they survive concurrent callers, a fork, and the end of the process.
|
|
295
|
+
|
|
296
|
+
_POOL_DATA = b"\n".join(b'{"n": %d, "s": "%s"}' % (i, b"x" * (i % 50)) for i in range(40000))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_threads_concurrent_callers():
|
|
300
|
+
import threading
|
|
301
|
+
|
|
302
|
+
expected = list(range(40000))
|
|
303
|
+
errors = []
|
|
304
|
+
|
|
305
|
+
def call():
|
|
306
|
+
for _ in range(5):
|
|
307
|
+
if fastsimdjson.map_many(lambda d: d["n"], _POOL_DATA, threads=8) != expected:
|
|
308
|
+
errors.append("mismatch")
|
|
309
|
+
|
|
310
|
+
ts = [threading.Thread(target=call) for _ in range(6)]
|
|
311
|
+
for t in ts:
|
|
312
|
+
t.start()
|
|
313
|
+
for t in ts:
|
|
314
|
+
t.join()
|
|
315
|
+
assert errors == []
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@pytest.mark.skipif(not hasattr(os, "fork"), reason="no fork")
|
|
319
|
+
def test_threads_after_fork():
|
|
320
|
+
import subprocess
|
|
321
|
+
import sys
|
|
322
|
+
|
|
323
|
+
code = (
|
|
324
|
+
"import os, fastsimdjson as F\n"
|
|
325
|
+
"data = b'\\n'.join(b'{\"n\": %d, \"s\": \"%s\"}' % (i, b'x' * (i % 50)) for i in range(6000))\n"
|
|
326
|
+
"n = len(list(F.loads_many(data)))\n"
|
|
327
|
+
"assert len(F.loads_many(data, threads=4)) == n\n"
|
|
328
|
+
"pid = os.fork()\n"
|
|
329
|
+
"if pid == 0:\n"
|
|
330
|
+
" os._exit(0 if len(F.loads_many(data, threads=4)) == n else 1)\n"
|
|
331
|
+
"_, status = os.waitpid(pid, 0)\n"
|
|
332
|
+
"assert os.waitstatus_to_exitcode(status) == 0\n"
|
|
333
|
+
"print('ok')\n"
|
|
334
|
+
)
|
|
335
|
+
r = subprocess.run([sys.executable, "-W", "ignore", "-c", code], capture_output=True, text=True, timeout=120)
|
|
336
|
+
assert r.returncode == 0 and r.stdout.strip() == "ok", r.stderr
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_threads_exit_cleanly():
|
|
340
|
+
import subprocess
|
|
341
|
+
import sys
|
|
342
|
+
|
|
343
|
+
code = (
|
|
344
|
+
"import fastsimdjson as F, threading\n"
|
|
345
|
+
"data = b'\\n'.join(b'{\"n\": %d, \"s\": \"%s\"}' % (i, b'x' * (i % 50)) for i in range(6000))\n"
|
|
346
|
+
"F.map_many(len, data, threads=8)\n"
|
|
347
|
+
"t = threading.Thread(target=lambda: F.loads_many(data, threads=4), daemon=True)\n"
|
|
348
|
+
"t.start()\n"
|
|
349
|
+
"print('ok')\n"
|
|
350
|
+
)
|
|
351
|
+
r = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=120)
|
|
352
|
+
assert r.returncode == 0 and r.stdout.strip() == "ok", r.stderr
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_map_many_lazy_views_kept():
|
|
356
|
+
# Views that fn keeps (whole or in part, or in an exception) remain valid
|
|
357
|
+
# after map_many, with or without threads.
|
|
358
|
+
data = b"\n".join(json.dumps({"n": i, "a": [i, {"b": str(i)}], "s": "x" * (i % 30)}).encode()
|
|
359
|
+
for i in range(30000))
|
|
360
|
+
expected = [json.loads(x) for x in data.split(b"\n")]
|
|
361
|
+
for threads in (None, 1, 4):
|
|
362
|
+
kept = fastsimdjson.map_many(lambda v: v, data, threads=threads, lazy=True)
|
|
363
|
+
assert [to_py(v) for v in kept] == expected
|
|
364
|
+
parts = fastsimdjson.map_many(lambda v: v["a"][1], data, threads=threads, lazy=True)
|
|
365
|
+
assert [to_py(v) for v in parts] == [e["a"][1] for e in expected]
|
|
366
|
+
keys = fastsimdjson.map_many(lambda v: list(v.items()), data, threads=threads, lazy=True)
|
|
367
|
+
assert [[(k, to_py(x)) for k, x in kv] for kv in keys] == [list(e.items()) for e in expected]
|
|
368
|
+
some = []
|
|
369
|
+
out = fastsimdjson.map_many(lambda v: some.append(v) if v["n"] % 1000 == 0 else v["n"],
|
|
370
|
+
data, threads=threads, lazy=True)
|
|
371
|
+
# With threads, the appends happen in any order.
|
|
372
|
+
assert sorted((to_py(v) for v in some), key=lambda e: e["n"]) == \
|
|
373
|
+
[e for e in expected if e["n"] % 1000 == 0]
|
|
374
|
+
assert out == [None if e["n"] % 1000 == 0 else e["n"] for e in expected]
|
|
375
|
+
|
|
376
|
+
def boom(v):
|
|
377
|
+
if v["n"] == 20000:
|
|
378
|
+
raise ValueError(v) # the exception keeps the view
|
|
379
|
+
return v["n"]
|
|
380
|
+
|
|
381
|
+
with pytest.raises(ValueError) as info:
|
|
382
|
+
fastsimdjson.map_many(boom, data, threads=threads, lazy=True)
|
|
383
|
+
assert to_py(info.value.args[0]) == expected[20000]
|
|
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
|