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.
Files changed (25) hide show
  1. {fastsimdjson-0.3.0/src/fastsimdjson.egg-info → fastsimdjson-0.4.0}/PKG-INFO +9 -6
  2. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/README.md +8 -5
  3. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/pyproject.toml +1 -1
  4. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.cpp +49 -6
  5. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0/src/fastsimdjson.egg-info}/PKG-INFO +9 -6
  6. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_dumps.py +20 -0
  7. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/MANIFEST.in +0 -0
  8. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/setup.cfg +0 -0
  9. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/setup.py +0 -0
  10. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.egg-info/SOURCES.txt +0 -0
  11. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.egg-info/dependency_links.txt +0 -0
  12. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.egg-info/requires.txt +0 -0
  13. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/src/fastsimdjson.egg-info/top_level.txt +0 -0
  14. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_files.py +0 -0
  15. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_lazy.py +0 -0
  16. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_loads.py +0 -0
  17. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/tests/test_stream.py +0 -0
  18. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdjson.cpp +0 -0
  19. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdjson.h +0 -0
  20. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdutf.cpp +0 -0
  21. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdutf.h +0 -0
  22. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/simdutf_c.h +0 -0
  23. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/zmij-LICENSE +0 -0
  24. {fastsimdjson-0.3.0 → fastsimdjson-0.4.0}/vendor/zmij.cc +0 -0
  25. {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.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. A parser that grows past 64 MB is freed on its own at the end of
441
- that call; its caches stay. A thread that exits without `release()` leaves
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. On a free-threaded build, `bytearray` and `memoryview`
446
- inputs are copied before parsing. Subinterpreters are not supported.
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. A parser that grows past 64 MB is freed on its own at the end of
410
- that call; its caches stay. A thread that exits without `release()` leaves
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. On a free-threaded build, `bytearray` and `memoryview`
415
- inputs are copied before parsing. Subinterpreters are not supported.
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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastsimdjson"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Fast JSON parsing for Python, built on simdjson"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -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
- release_parser();
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.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. A parser that grows past 64 MB is freed on its own at the end of
441
- that call; its caches stay. A thread that exits without `release()` leaves
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. On a free-threaded build, `bytearray` and `memoryview`
446
- inputs are copied before parsing. Subinterpreters are not supported.
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