fastsimdjson 0.3.0__tar.gz → 0.4.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.3.0/src/fastsimdjson.egg-info → fastsimdjson-0.4.0}/PKG-INFO +9 -6
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/README.md +8 -5
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/pyproject.toml +1 -1
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.cpp +49 -6
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0/src/fastsimdjson.egg-info}/PKG-INFO +9 -6
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_dumps.py +20 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/MANIFEST.in +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/setup.cfg +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/setup.py +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.egg-info/SOURCES.txt +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.egg-info/dependency_links.txt +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.egg-info/requires.txt +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.egg-info/top_level.txt +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_files.py +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_lazy.py +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_loads.py +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_stream.py +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdjson.cpp +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdjson.h +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdutf.cpp +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdutf.h +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdutf_c.h +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/zmij-LICENSE +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/zmij.cc +0 -0
- {fastsimdjson-0.3.0 → fastsimdjson-0.4.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.4.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
|
|
@@ -437,10 +437,13 @@ its time.
|
|
|
437
437
|
|
|
438
438
|
* The simdjson parser and the key and string caches are thread-local.
|
|
439
439
|
`release()` frees the parser and the cached strings retained by the calling
|
|
440
|
-
thread
|
|
441
|
-
|
|
442
|
-
its cached strings behind.
|
|
440
|
+
thread, and they are freed when a Python thread exits. A parser that grows
|
|
441
|
+
past 64 MB is freed on its own at the end of that call; its caches stay.
|
|
443
442
|
* The module is marked free-threading compatible (`Py_MOD_GIL_NOT_USED` on
|
|
444
443
|
Python 3.13 and newer), so importing it on a free-threaded build does not
|
|
445
|
-
re-enable the GIL.
|
|
446
|
-
|
|
444
|
+
re-enable the GIL. It is tested with many threads parsing, serializing and
|
|
445
|
+
sharing views and stream iterators on free-threaded Python 3.14 and 3.15.
|
|
446
|
+
On a free-threaded build, `bytearray` and `memoryview` inputs are copied
|
|
447
|
+
before parsing, and `dumps` and `dumpb` hold references to the items of the
|
|
448
|
+
containers they serialize, since other threads may change them.
|
|
449
|
+
Subinterpreters are not supported.
|
|
@@ -406,10 +406,13 @@ its time.
|
|
|
406
406
|
|
|
407
407
|
* The simdjson parser and the key and string caches are thread-local.
|
|
408
408
|
`release()` frees the parser and the cached strings retained by the calling
|
|
409
|
-
thread
|
|
410
|
-
|
|
411
|
-
its cached strings behind.
|
|
409
|
+
thread, and they are freed when a Python thread exits. A parser that grows
|
|
410
|
+
past 64 MB is freed on its own at the end of that call; its caches stay.
|
|
412
411
|
* The module is marked free-threading compatible (`Py_MOD_GIL_NOT_USED` on
|
|
413
412
|
Python 3.13 and newer), so importing it on a free-threaded build does not
|
|
414
|
-
re-enable the GIL.
|
|
415
|
-
|
|
413
|
+
re-enable the GIL. It is tested with many threads parsing, serializing and
|
|
414
|
+
sharing views and stream iterators on free-threaded Python 3.14 and 3.15.
|
|
415
|
+
On a free-threaded build, `bytearray` and `memoryview` inputs are copied
|
|
416
|
+
before parsing, and `dumps` and `dumpb` hold references to the items of the
|
|
417
|
+
containers they serialize, since other threads may change them.
|
|
418
|
+
Subinterpreters are not supported.
|
|
@@ -147,6 +147,7 @@ struct ThreadParser {
|
|
|
147
147
|
KeyCacheEntry key_cache[KEY_CACHE_SIZE]{};
|
|
148
148
|
KeyCacheEntry value_cache[KEY_CACHE_SIZE]{};
|
|
149
149
|
EncodedKey *encoded_keys = nullptr; // allocated on first use by dumpb
|
|
150
|
+
bool cleanup_registered = false; // see register_thread_cleanup
|
|
150
151
|
void clear_encoded_keys() {
|
|
151
152
|
if (encoded_keys != nullptr) {
|
|
152
153
|
for (size_t i = 0; i < ENCODED_KEY_CACHE_SIZE; i++) {
|
|
@@ -170,6 +171,50 @@ struct ThreadParser {
|
|
|
170
171
|
};
|
|
171
172
|
thread_local ThreadParser g_thread_parser;
|
|
172
173
|
|
|
174
|
+
// Frees this thread's parser, documents and caches (release()).
|
|
175
|
+
void release_thread_state() {
|
|
176
|
+
delete g_thread_parser.ptr;
|
|
177
|
+
g_thread_parser.ptr = nullptr;
|
|
178
|
+
delete g_thread_parser.spare;
|
|
179
|
+
g_thread_parser.spare = nullptr;
|
|
180
|
+
clear_cache(g_thread_parser.key_cache);
|
|
181
|
+
clear_cache(g_thread_parser.value_cache);
|
|
182
|
+
g_thread_parser.clear_encoded_keys();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The thread-local destructor runs after Python has deleted the thread
|
|
186
|
+
// state of an exiting thread, too late to release the cached strings. So
|
|
187
|
+
// each thread puts a capsule in its thread state's dict: Python clears that
|
|
188
|
+
// dict while the thread state is still valid, and the capsule's destructor
|
|
189
|
+
// releases the caches then. It does so only on its own thread (a thread
|
|
190
|
+
// state can be cleared from another thread, at finalization).
|
|
191
|
+
const char THREAD_CAPSULE[] = "fastsimdjson.thread_state";
|
|
192
|
+
|
|
193
|
+
void thread_cleanup(PyObject *capsule) {
|
|
194
|
+
if (PyCapsule_GetPointer(capsule, THREAD_CAPSULE) == &g_thread_parser) {
|
|
195
|
+
g_thread_parser.cleanup_registered = false;
|
|
196
|
+
if (caches_can_decref()) {
|
|
197
|
+
release_thread_state();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
FSJ_NOINLINE void register_thread_cleanup_slow() {
|
|
203
|
+
g_thread_parser.cleanup_registered = true;
|
|
204
|
+
PyObject *dict = PyThreadState_GetDict(); // borrowed
|
|
205
|
+
PyObject *capsule = dict ? PyCapsule_New(&g_thread_parser, THREAD_CAPSULE, thread_cleanup) : nullptr;
|
|
206
|
+
if (capsule == nullptr || PyDict_SetItemString(dict, THREAD_CAPSULE, capsule) < 0) {
|
|
207
|
+
PyErr_Clear(); // without it, the caches of an exiting thread leak, as before
|
|
208
|
+
}
|
|
209
|
+
Py_XDECREF(capsule);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
inline void register_thread_cleanup() {
|
|
213
|
+
if (!g_thread_parser.cleanup_registered) {
|
|
214
|
+
register_thread_cleanup_slow();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
173
218
|
inline uint64_t load_u64(const char *p) {
|
|
174
219
|
uint64_t v;
|
|
175
220
|
memcpy(&v, p, 8);
|
|
@@ -284,6 +329,7 @@ inline PyObject *make_str(const char *s, size_t len) {
|
|
|
284
329
|
}
|
|
285
330
|
|
|
286
331
|
inline void cache_store(KeyCacheEntry &entry, PyObject *u, size_t len, const KeyWords &w) {
|
|
332
|
+
register_thread_cleanup(); // the entry holds a reference: free it at thread exit
|
|
287
333
|
Py_INCREF(u);
|
|
288
334
|
Py_XDECREF(entry.key);
|
|
289
335
|
entry.key = u;
|
|
@@ -593,6 +639,7 @@ bool ensure_parser() {
|
|
|
593
639
|
if (g_thread_parser.ptr != nullptr) {
|
|
594
640
|
return true;
|
|
595
641
|
}
|
|
642
|
+
register_thread_cleanup();
|
|
596
643
|
g_thread_parser.ptr = new (std::nothrow) parser();
|
|
597
644
|
if (g_thread_parser.ptr == nullptr) {
|
|
598
645
|
PyErr_NoMemory();
|
|
@@ -2698,6 +2745,7 @@ struct OrjsonEncoder {
|
|
|
2698
2745
|
return -1;
|
|
2699
2746
|
}
|
|
2700
2747
|
if (keys == nullptr) {
|
|
2748
|
+
register_thread_cleanup();
|
|
2701
2749
|
keys = g_thread_parser.encoded_keys = new (std::nothrow) EncodedKey[ENCODED_KEY_CACHE_SIZE];
|
|
2702
2750
|
}
|
|
2703
2751
|
int rc = 0;
|
|
@@ -3428,12 +3476,7 @@ PyObject *parse_only(PyObject *, PyObject *arg) {
|
|
|
3428
3476
|
}
|
|
3429
3477
|
|
|
3430
3478
|
PyObject *release(PyObject *, PyObject *) {
|
|
3431
|
-
|
|
3432
|
-
delete g_thread_parser.spare;
|
|
3433
|
-
g_thread_parser.spare = nullptr;
|
|
3434
|
-
clear_cache(g_thread_parser.key_cache);
|
|
3435
|
-
clear_cache(g_thread_parser.value_cache);
|
|
3436
|
-
g_thread_parser.clear_encoded_keys();
|
|
3479
|
+
release_thread_state();
|
|
3437
3480
|
Py_RETURN_NONE;
|
|
3438
3481
|
}
|
|
3439
3482
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastsimdjson
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -437,10 +437,13 @@ its time.
|
|
|
437
437
|
|
|
438
438
|
* The simdjson parser and the key and string caches are thread-local.
|
|
439
439
|
`release()` frees the parser and the cached strings retained by the calling
|
|
440
|
-
thread
|
|
441
|
-
|
|
442
|
-
its cached strings behind.
|
|
440
|
+
thread, and they are freed when a Python thread exits. A parser that grows
|
|
441
|
+
past 64 MB is freed on its own at the end of that call; its caches stay.
|
|
443
442
|
* The module is marked free-threading compatible (`Py_MOD_GIL_NOT_USED` on
|
|
444
443
|
Python 3.13 and newer), so importing it on a free-threaded build does not
|
|
445
|
-
re-enable the GIL.
|
|
446
|
-
|
|
444
|
+
re-enable the GIL. It is tested with many threads parsing, serializing and
|
|
445
|
+
sharing views and stream iterators on free-threaded Python 3.14 and 3.15.
|
|
446
|
+
On a free-threaded build, `bytearray` and `memoryview` inputs are copied
|
|
447
|
+
before parsing, and `dumps` and `dumpb` hold references to the items of the
|
|
448
|
+
containers they serialize, since other threads may change them.
|
|
449
|
+
Subinterpreters are not supported.
|
|
@@ -415,3 +415,23 @@ def test_dumpb_matches_orjson_cases():
|
|
|
415
415
|
for obj, option, _ in ORJSON_CASES:
|
|
416
416
|
assert orjson_outcome(fastsimdjson.dumpb, obj, option=option) == \
|
|
417
417
|
orjson_outcome(orjson.dumps, obj, option=option), (obj, option)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_thread_exit_releases_caches():
|
|
421
|
+
# A key that enters dumpb's key cache (seen twice) is referenced by the
|
|
422
|
+
# cache; when the thread exits, its caches are released.
|
|
423
|
+
import sys
|
|
424
|
+
import threading
|
|
425
|
+
|
|
426
|
+
key = "".join(["thread", "-exit-", "key"]) # not interned
|
|
427
|
+
before = sys.getrefcount(key)
|
|
428
|
+
|
|
429
|
+
def work():
|
|
430
|
+
for _ in range(3):
|
|
431
|
+
assert fastsimdjson.dumpb({key: 1}) == b'{"thread-exit-key":1}'
|
|
432
|
+
fastsimdjson.loads(b'{"a": "b"}')
|
|
433
|
+
|
|
434
|
+
t = threading.Thread(target=work)
|
|
435
|
+
t.start()
|
|
436
|
+
t.join()
|
|
437
|
+
assert sys.getrefcount(key) == before
|
|
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
|