fastsimdjson 0.7.0__tar.gz → 0.8.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.7.0/src/fastsimdjson.egg-info → fastsimdjson-0.8.0}/PKG-INFO +49 -7
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/README.md +48 -6
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/pyproject.toml +1 -1
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/src/fastsimdjson.cpp +379 -38
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0/src/fastsimdjson.egg-info}/PKG-INFO +49 -7
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/tests/test_dumps.py +112 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/MANIFEST.in +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/setup.cfg +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/setup.py +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/src/fastsimdjson.egg-info/SOURCES.txt +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/src/fastsimdjson.egg-info/dependency_links.txt +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/src/fastsimdjson.egg-info/requires.txt +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/src/fastsimdjson.egg-info/top_level.txt +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/tests/test_files.py +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/tests/test_lazy.py +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/tests/test_loads.py +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/tests/test_stream.py +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/vendor/simdjson.cpp +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/vendor/simdjson.h +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/vendor/simdutf.cpp +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/vendor/simdutf.h +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/vendor/simdutf_c.h +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/vendor/zmij-LICENSE +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.0}/vendor/zmij.cc +0 -0
- {fastsimdjson-0.7.0 → fastsimdjson-0.8.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.8.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
|
|
@@ -42,7 +42,8 @@ function is faster still. It also reads streams of documents (NDJSON, JSON
|
|
|
42
42
|
Lines), on many threads with free-threaded Python, and can extract only the
|
|
43
43
|
fields you need from each document. It writes JSON in two ways: `dumps` returns the same `str` as
|
|
44
44
|
`json.dumps`, and `dumpb` returns the same `bytes` as `orjson.dumps`, as fast
|
|
45
|
-
as orjson
|
|
45
|
+
as orjson; `dumpb_many` writes NDJSON, on many threads with free-threaded
|
|
46
|
+
Python.
|
|
46
47
|
|
|
47
48
|
With pip:
|
|
48
49
|
|
|
@@ -158,6 +159,30 @@ serialize dataclasses, `datetime`, `date`, `time`, `UUID`, numpy arrays or
|
|
|
158
159
|
`OPT_PASSTHROUGH_DATACLASS` and `OPT_PASSTHROUGH_DATETIME` were set, and the
|
|
159
160
|
options that concern only these types are accepted and have no effect.
|
|
160
161
|
|
|
162
|
+
### `dumpb_many`: NDJSON, on several threads
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
data = fastsimdjson.dumpb_many(records) # b'{...}\n{...}\n'
|
|
166
|
+
data = fastsimdjson.dumpb_many(records, threads=16) # free-threaded Python
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`dumpb_many(records, default=None, option=None, *, threads=None)` returns
|
|
170
|
+
the records (a list, a tuple or any iterable) as NDJSON `bytes`: each record
|
|
171
|
+
as `dumpb` writes it, followed by a newline, so that the output reads back
|
|
172
|
+
with `loads_many(data, format="lines")`. `default` and `option` are those of
|
|
173
|
+
`dumpb`, except `OPT_INDENT_2`, which would spread a record over several
|
|
174
|
+
lines (`ValueError`); `OPT_APPEND_NEWLINE` changes nothing.
|
|
175
|
+
|
|
176
|
+
On free-threaded Python, `threads=N` writes chunks of records on N threads at
|
|
177
|
+
once (`threads=0` or `None`: one per processor), with the worker threads of
|
|
178
|
+
`map_many`. The output and the errors are those of one thread: when records
|
|
179
|
+
cannot be serialized, the error of the first of them is raised (`default` may
|
|
180
|
+
then have been called on later records, in other threads, and it must be
|
|
181
|
+
safe to call from several threads at once). With the GIL, the calling thread
|
|
182
|
+
does all the work. Like `dumpb`, `dumpb_many` does not crash when other
|
|
183
|
+
threads change the records meanwhile, but the output then reflects some of
|
|
184
|
+
the changes and not others.
|
|
185
|
+
|
|
161
186
|
### Files
|
|
162
187
|
|
|
163
188
|
```python
|
|
@@ -380,7 +405,7 @@ geometric mean over the 22 files (higher is better).
|
|
|
380
405
|
|
|
381
406
|
<picture>
|
|
382
407
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads_dark.png">
|
|
383
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
408
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, jiter without its string cache 0.44, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
384
409
|
</picture>
|
|
385
410
|
|
|
386
411
|
| parser | GB/s | vs `json.loads` |
|
|
@@ -391,7 +416,8 @@ geometric mean over the 22 files (higher is better).
|
|
|
391
416
|
| ujson 6.0.0 | 0.37 | 1.66× |
|
|
392
417
|
| cysimdjson 26.27 | 0.43 | 1.96× |
|
|
393
418
|
| pysimdjson 7.0.2 | 0.44 | 1.98× |
|
|
394
|
-
| jiter 0.17.0 | 0.
|
|
419
|
+
| jiter 0.17.0 (no string cache) | 0.44 | 2.00× |
|
|
420
|
+
| jiter 0.17.0 (string cache, the default) | 0.48 | 2.21× |
|
|
395
421
|
| msgspec 0.22.0 | 0.53 | 2.42× |
|
|
396
422
|
| orjson 3.12.0 | 0.60 | 2.74× |
|
|
397
423
|
| **fastsimdjson `loads`** | **0.78** | **3.53×** |
|
|
@@ -407,7 +433,7 @@ the collector enabled and 1.22× with it disabled (geometric means); without
|
|
|
407
433
|
the collector, `canada.json`, `mesh.json` and `numbers.json` are ties. yyjson 4.0.6 is left out: it
|
|
408
434
|
returns wrong strings for non-ASCII text. jiter (the parser of pydantic)
|
|
409
435
|
caches strings by default; turning the cache off (`cache_mode=False`) makes it
|
|
410
|
-
slower here
|
|
436
|
+
slower here.
|
|
411
437
|
|
|
412
438
|
Parsing is no longer the bottleneck. simdjson alone parses these files at
|
|
413
439
|
3.1 GB/s. It accounts for about a third of the time of `loads`; the rest goes
|
|
@@ -422,12 +448,14 @@ and the screen name of the 100 statuses of `twitter.json`:
|
|
|
422
448
|
|
|
423
449
|
<picture>
|
|
424
450
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_parse_dark.png">
|
|
425
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_parse.png" width="80%" alt="Reading part of twitter.json: fastsimdjson parse 158 µs, pysimdjson 179, cysimdjson 229, msgspec 336, fastsimdjson loads 861, orjson 1009, json 3922">
|
|
451
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_parse.png" width="80%" alt="Reading part of twitter.json: fastsimdjson parse 158 µs, pysimdjson 179, cysimdjson 229, msgspec 336, fastsimdjson loads 861, orjson 1009, jiter 1281, jiter without its string cache 1592, json 3922">
|
|
426
452
|
</picture>
|
|
427
453
|
|
|
428
454
|
| method | µs |
|
|
429
455
|
|---|---:|
|
|
430
456
|
| `json.loads` | 3922 |
|
|
457
|
+
| jiter (no string cache) | 1592 |
|
|
458
|
+
| jiter (string cache) | 1281 |
|
|
431
459
|
| orjson | 1009 |
|
|
432
460
|
| fastsimdjson `loads` | 861 |
|
|
433
461
|
| msgspec (typed `Struct`) | 336 |
|
|
@@ -481,6 +509,18 @@ times faster than msgspec and 10 times faster than `json`. The comparison was
|
|
|
481
509
|
run on a processor with AVX-512, which `dumpb` and orjson both use to escape
|
|
482
510
|
strings; other x64 processors use SSE2 and ARM processors NEON.
|
|
483
511
|
|
|
512
|
+
### Writing NDJSON with `dumpb_many`
|
|
513
|
+
|
|
514
|
+
The 593,945 events of two hours of [GH Archive](https://www.gharchive.org/)
|
|
515
|
+
(1.87 GB of NDJSON), written back as NDJSON from Python objects. With the
|
|
516
|
+
GIL (Python 3.14.6), one thread: `orjson.dumps` on each record, joined, runs
|
|
517
|
+
at 0.94 GB/s, `dumpb` on each record at 1.07 GB/s, and `dumpb_many` at
|
|
518
|
+
1.41 GB/s. On free-threaded Python 3.14.6t, GB/s:
|
|
519
|
+
|
|
520
|
+
| threads | 1 | 8 | 32 | 64 |
|
|
521
|
+
|---|---:|---:|---:|---:|
|
|
522
|
+
| `dumpb_many(records, threads=N)` | 1.3 | 6.0 | 11 | 12 |
|
|
523
|
+
|
|
484
524
|
### Streams with `loads_many`
|
|
485
525
|
|
|
486
526
|
20 MB of NDJSON (5268 objects and arrays, one per line), made from the same
|
|
@@ -488,12 +528,14 @@ files; best of three runs:
|
|
|
488
528
|
|
|
489
529
|
<picture>
|
|
490
530
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_ndjson_dark.png">
|
|
491
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_ndjson.png" width="80%" alt="Reading NDJSON: fastsimdjson parse_many 0.94 GB/s, loads_many 0.38, msgspec decode_lines 0.37, fastsimdjson loads per line 0.33, msgspec decode per line 0.27, orjson per line 0.26, json per line 0.12">
|
|
531
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_ndjson.png" width="80%" alt="Reading NDJSON: fastsimdjson parse_many 0.94 GB/s, loads_many 0.38, msgspec decode_lines 0.37, fastsimdjson loads per line 0.33, msgspec decode per line 0.27, orjson per line 0.26, jiter per line 0.22, jiter without its string cache per line 0.20, json per line 0.12">
|
|
492
532
|
</picture>
|
|
493
533
|
|
|
494
534
|
| method | ms | GB/s |
|
|
495
535
|
|---|---:|---:|
|
|
496
536
|
| `json.loads` on each line | 169 | 0.12 |
|
|
537
|
+
| jiter on each line (no string cache) | 98 | 0.20 |
|
|
538
|
+
| jiter on each line (string cache) | 90 | 0.22 |
|
|
497
539
|
| orjson on each line | 77 | 0.26 |
|
|
498
540
|
| msgspec `decode` on each line | 74 | 0.27 |
|
|
499
541
|
| fastsimdjson `loads` on each line | 60 | 0.33 |
|
|
@@ -10,7 +10,8 @@ function is faster still. It also reads streams of documents (NDJSON, JSON
|
|
|
10
10
|
Lines), on many threads with free-threaded Python, and can extract only the
|
|
11
11
|
fields you need from each document. It writes JSON in two ways: `dumps` returns the same `str` as
|
|
12
12
|
`json.dumps`, and `dumpb` returns the same `bytes` as `orjson.dumps`, as fast
|
|
13
|
-
as orjson
|
|
13
|
+
as orjson; `dumpb_many` writes NDJSON, on many threads with free-threaded
|
|
14
|
+
Python.
|
|
14
15
|
|
|
15
16
|
With pip:
|
|
16
17
|
|
|
@@ -126,6 +127,30 @@ serialize dataclasses, `datetime`, `date`, `time`, `UUID`, numpy arrays or
|
|
|
126
127
|
`OPT_PASSTHROUGH_DATACLASS` and `OPT_PASSTHROUGH_DATETIME` were set, and the
|
|
127
128
|
options that concern only these types are accepted and have no effect.
|
|
128
129
|
|
|
130
|
+
### `dumpb_many`: NDJSON, on several threads
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
data = fastsimdjson.dumpb_many(records) # b'{...}\n{...}\n'
|
|
134
|
+
data = fastsimdjson.dumpb_many(records, threads=16) # free-threaded Python
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`dumpb_many(records, default=None, option=None, *, threads=None)` returns
|
|
138
|
+
the records (a list, a tuple or any iterable) as NDJSON `bytes`: each record
|
|
139
|
+
as `dumpb` writes it, followed by a newline, so that the output reads back
|
|
140
|
+
with `loads_many(data, format="lines")`. `default` and `option` are those of
|
|
141
|
+
`dumpb`, except `OPT_INDENT_2`, which would spread a record over several
|
|
142
|
+
lines (`ValueError`); `OPT_APPEND_NEWLINE` changes nothing.
|
|
143
|
+
|
|
144
|
+
On free-threaded Python, `threads=N` writes chunks of records on N threads at
|
|
145
|
+
once (`threads=0` or `None`: one per processor), with the worker threads of
|
|
146
|
+
`map_many`. The output and the errors are those of one thread: when records
|
|
147
|
+
cannot be serialized, the error of the first of them is raised (`default` may
|
|
148
|
+
then have been called on later records, in other threads, and it must be
|
|
149
|
+
safe to call from several threads at once). With the GIL, the calling thread
|
|
150
|
+
does all the work. Like `dumpb`, `dumpb_many` does not crash when other
|
|
151
|
+
threads change the records meanwhile, but the output then reflects some of
|
|
152
|
+
the changes and not others.
|
|
153
|
+
|
|
129
154
|
### Files
|
|
130
155
|
|
|
131
156
|
```python
|
|
@@ -348,7 +373,7 @@ geometric mean over the 22 files (higher is better).
|
|
|
348
373
|
|
|
349
374
|
<picture>
|
|
350
375
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads_dark.png">
|
|
351
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
376
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, jiter without its string cache 0.44, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
352
377
|
</picture>
|
|
353
378
|
|
|
354
379
|
| parser | GB/s | vs `json.loads` |
|
|
@@ -359,7 +384,8 @@ geometric mean over the 22 files (higher is better).
|
|
|
359
384
|
| ujson 6.0.0 | 0.37 | 1.66× |
|
|
360
385
|
| cysimdjson 26.27 | 0.43 | 1.96× |
|
|
361
386
|
| pysimdjson 7.0.2 | 0.44 | 1.98× |
|
|
362
|
-
| jiter 0.17.0 | 0.
|
|
387
|
+
| jiter 0.17.0 (no string cache) | 0.44 | 2.00× |
|
|
388
|
+
| jiter 0.17.0 (string cache, the default) | 0.48 | 2.21× |
|
|
363
389
|
| msgspec 0.22.0 | 0.53 | 2.42× |
|
|
364
390
|
| orjson 3.12.0 | 0.60 | 2.74× |
|
|
365
391
|
| **fastsimdjson `loads`** | **0.78** | **3.53×** |
|
|
@@ -375,7 +401,7 @@ the collector enabled and 1.22× with it disabled (geometric means); without
|
|
|
375
401
|
the collector, `canada.json`, `mesh.json` and `numbers.json` are ties. yyjson 4.0.6 is left out: it
|
|
376
402
|
returns wrong strings for non-ASCII text. jiter (the parser of pydantic)
|
|
377
403
|
caches strings by default; turning the cache off (`cache_mode=False`) makes it
|
|
378
|
-
slower here
|
|
404
|
+
slower here.
|
|
379
405
|
|
|
380
406
|
Parsing is no longer the bottleneck. simdjson alone parses these files at
|
|
381
407
|
3.1 GB/s. It accounts for about a third of the time of `loads`; the rest goes
|
|
@@ -390,12 +416,14 @@ and the screen name of the 100 statuses of `twitter.json`:
|
|
|
390
416
|
|
|
391
417
|
<picture>
|
|
392
418
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_parse_dark.png">
|
|
393
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_parse.png" width="80%" alt="Reading part of twitter.json: fastsimdjson parse 158 µs, pysimdjson 179, cysimdjson 229, msgspec 336, fastsimdjson loads 861, orjson 1009, json 3922">
|
|
419
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_parse.png" width="80%" alt="Reading part of twitter.json: fastsimdjson parse 158 µs, pysimdjson 179, cysimdjson 229, msgspec 336, fastsimdjson loads 861, orjson 1009, jiter 1281, jiter without its string cache 1592, json 3922">
|
|
394
420
|
</picture>
|
|
395
421
|
|
|
396
422
|
| method | µs |
|
|
397
423
|
|---|---:|
|
|
398
424
|
| `json.loads` | 3922 |
|
|
425
|
+
| jiter (no string cache) | 1592 |
|
|
426
|
+
| jiter (string cache) | 1281 |
|
|
399
427
|
| orjson | 1009 |
|
|
400
428
|
| fastsimdjson `loads` | 861 |
|
|
401
429
|
| msgspec (typed `Struct`) | 336 |
|
|
@@ -449,6 +477,18 @@ times faster than msgspec and 10 times faster than `json`. The comparison was
|
|
|
449
477
|
run on a processor with AVX-512, which `dumpb` and orjson both use to escape
|
|
450
478
|
strings; other x64 processors use SSE2 and ARM processors NEON.
|
|
451
479
|
|
|
480
|
+
### Writing NDJSON with `dumpb_many`
|
|
481
|
+
|
|
482
|
+
The 593,945 events of two hours of [GH Archive](https://www.gharchive.org/)
|
|
483
|
+
(1.87 GB of NDJSON), written back as NDJSON from Python objects. With the
|
|
484
|
+
GIL (Python 3.14.6), one thread: `orjson.dumps` on each record, joined, runs
|
|
485
|
+
at 0.94 GB/s, `dumpb` on each record at 1.07 GB/s, and `dumpb_many` at
|
|
486
|
+
1.41 GB/s. On free-threaded Python 3.14.6t, GB/s:
|
|
487
|
+
|
|
488
|
+
| threads | 1 | 8 | 32 | 64 |
|
|
489
|
+
|---|---:|---:|---:|---:|
|
|
490
|
+
| `dumpb_many(records, threads=N)` | 1.3 | 6.0 | 11 | 12 |
|
|
491
|
+
|
|
452
492
|
### Streams with `loads_many`
|
|
453
493
|
|
|
454
494
|
20 MB of NDJSON (5268 objects and arrays, one per line), made from the same
|
|
@@ -456,12 +496,14 @@ files; best of three runs:
|
|
|
456
496
|
|
|
457
497
|
<picture>
|
|
458
498
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_ndjson_dark.png">
|
|
459
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_ndjson.png" width="80%" alt="Reading NDJSON: fastsimdjson parse_many 0.94 GB/s, loads_many 0.38, msgspec decode_lines 0.37, fastsimdjson loads per line 0.33, msgspec decode per line 0.27, orjson per line 0.26, json per line 0.12">
|
|
499
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_ndjson.png" width="80%" alt="Reading NDJSON: fastsimdjson parse_many 0.94 GB/s, loads_many 0.38, msgspec decode_lines 0.37, fastsimdjson loads per line 0.33, msgspec decode per line 0.27, orjson per line 0.26, jiter per line 0.22, jiter without its string cache per line 0.20, json per line 0.12">
|
|
460
500
|
</picture>
|
|
461
501
|
|
|
462
502
|
| method | ms | GB/s |
|
|
463
503
|
|---|---:|---:|
|
|
464
504
|
| `json.loads` on each line | 169 | 0.12 |
|
|
505
|
+
| jiter on each line (no string cache) | 98 | 0.20 |
|
|
506
|
+
| jiter on each line (string cache) | 90 | 0.22 |
|
|
465
507
|
| orjson on each line | 77 | 0.26 |
|
|
466
508
|
| msgspec `decode` on each line | 74 | 0.27 |
|
|
467
509
|
| fastsimdjson `loads` on each line | 60 | 0.33 |
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
#include <new>
|
|
13
13
|
#include <condition_variable>
|
|
14
14
|
#include <deque>
|
|
15
|
+
#include <memory>
|
|
15
16
|
#include <mutex>
|
|
16
17
|
#include <string>
|
|
17
18
|
#include <thread>
|
|
@@ -2940,6 +2941,11 @@ struct OrjsonEncoder {
|
|
|
2940
2941
|
}
|
|
2941
2942
|
}
|
|
2942
2943
|
} else {
|
|
2944
|
+
#ifdef Py_GIL_DISABLED
|
|
2945
|
+
if (encode_items_locked(o, is_list) < 0) {
|
|
2946
|
+
return -1;
|
|
2947
|
+
}
|
|
2948
|
+
#else
|
|
2943
2949
|
for (Py_ssize_t i = 0;; i++) {
|
|
2944
2950
|
PyObject *item = is_list ? list_item(o, i)
|
|
2945
2951
|
: i < n ? Py_NewRef(PyTuple_GET_ITEM(o, i))
|
|
@@ -2954,6 +2960,7 @@ struct OrjsonEncoder {
|
|
|
2954
2960
|
return -1;
|
|
2955
2961
|
}
|
|
2956
2962
|
}
|
|
2963
|
+
#endif
|
|
2957
2964
|
}
|
|
2958
2965
|
close(']');
|
|
2959
2966
|
return 0;
|
|
@@ -3010,6 +3017,93 @@ struct OrjsonEncoder {
|
|
|
3010
3017
|
return 0;
|
|
3011
3018
|
}
|
|
3012
3019
|
|
|
3020
|
+
#ifdef Py_GIL_DISABLED
|
|
3021
|
+
// Free-threaded Python: a dict or list is locked while its items are
|
|
3022
|
+
// encoded, and the items are borrowed when that is safe. Taking references
|
|
3023
|
+
// instead (a snapshot of the items) is slow on many threads: the records
|
|
3024
|
+
// of a list often share their keys and short strings, and the threads then
|
|
3025
|
+
// all update the same reference counts. Encoding an item that may lock
|
|
3026
|
+
// another object or run Python code can suspend the lock (another thread
|
|
3027
|
+
// may then change the container), so such an item is referenced first:
|
|
3028
|
+
// anything but an exact int, float, ASCII str, None, True or False.
|
|
3029
|
+
static bool borrowable(PyObject *v) {
|
|
3030
|
+
PyTypeObject *t = Py_TYPE(v);
|
|
3031
|
+
return (t == &PyUnicode_Type && PyUnicode_IS_COMPACT_ASCII(v)) || t == &PyLong_Type ||
|
|
3032
|
+
t == &PyFloat_Type || v == Py_None || v == Py_True || v == Py_False;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
// References released when they go out of scope, also when the output
|
|
3036
|
+
// buffer throws std::bad_alloc.
|
|
3037
|
+
struct Refs {
|
|
3038
|
+
PyObject *a, *b;
|
|
3039
|
+
~Refs() {
|
|
3040
|
+
Py_XDECREF(a);
|
|
3041
|
+
Py_XDECREF(b);
|
|
3042
|
+
}
|
|
3043
|
+
};
|
|
3044
|
+
|
|
3045
|
+
// The lock is released before std::bad_alloc goes further.
|
|
3046
|
+
FSJ_NOINLINE int encode_entries_locked(PyObject *o) {
|
|
3047
|
+
int rc = 0;
|
|
3048
|
+
bool oom = false;
|
|
3049
|
+
Py_BEGIN_CRITICAL_SECTION(o);
|
|
3050
|
+
try {
|
|
3051
|
+
Py_ssize_t pos = 0;
|
|
3052
|
+
PyObject *key, *value;
|
|
3053
|
+
bool first = true;
|
|
3054
|
+
while (rc == 0 && PyDict_Next(o, &pos, &key, &value)) {
|
|
3055
|
+
if (borrowable(key) && borrowable(value)) {
|
|
3056
|
+
rc = encode_entry(key, value, first);
|
|
3057
|
+
} else {
|
|
3058
|
+
Refs r{Py_NewRef(key), Py_NewRef(value)};
|
|
3059
|
+
rc = encode_entry(key, value, first);
|
|
3060
|
+
}
|
|
3061
|
+
first = false;
|
|
3062
|
+
}
|
|
3063
|
+
} catch (const std::bad_alloc &) {
|
|
3064
|
+
oom = true;
|
|
3065
|
+
}
|
|
3066
|
+
Py_END_CRITICAL_SECTION();
|
|
3067
|
+
if (oom) {
|
|
3068
|
+
throw std::bad_alloc();
|
|
3069
|
+
}
|
|
3070
|
+
return rc;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// A tuple cannot change: its items live as long as it does.
|
|
3074
|
+
FSJ_NOINLINE int encode_items_locked(PyObject *o, bool is_list) {
|
|
3075
|
+
int rc = 0;
|
|
3076
|
+
if (!is_list) {
|
|
3077
|
+
for (Py_ssize_t i = 0; rc == 0 && i < PyTuple_GET_SIZE(o); i++) {
|
|
3078
|
+
separate(i == 0);
|
|
3079
|
+
rc = encode(PyTuple_GET_ITEM(o, i));
|
|
3080
|
+
}
|
|
3081
|
+
return rc;
|
|
3082
|
+
}
|
|
3083
|
+
bool oom = false;
|
|
3084
|
+
Py_BEGIN_CRITICAL_SECTION(o);
|
|
3085
|
+
try {
|
|
3086
|
+
for (Py_ssize_t i = 0; rc == 0 && i < PyList_GET_SIZE(o); i++) {
|
|
3087
|
+
PyObject *item = PyList_GET_ITEM(o, i);
|
|
3088
|
+
separate(i == 0);
|
|
3089
|
+
if (borrowable(item)) {
|
|
3090
|
+
rc = encode(item);
|
|
3091
|
+
} else {
|
|
3092
|
+
Refs r{Py_NewRef(item), nullptr};
|
|
3093
|
+
rc = encode(item);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
} catch (const std::bad_alloc &) {
|
|
3097
|
+
oom = true;
|
|
3098
|
+
}
|
|
3099
|
+
Py_END_CRITICAL_SECTION();
|
|
3100
|
+
if (oom) {
|
|
3101
|
+
throw std::bad_alloc();
|
|
3102
|
+
}
|
|
3103
|
+
return rc;
|
|
3104
|
+
}
|
|
3105
|
+
#endif
|
|
3106
|
+
|
|
3013
3107
|
// Dict subclasses as dicts (their items, as stored), as with orjson.
|
|
3014
3108
|
FSJ_NOINLINE int encode_dict(PyObject *o) {
|
|
3015
3109
|
if (depth >= ORJSON_MAX_DEPTH) { // empty containers count too
|
|
@@ -3027,6 +3121,15 @@ struct OrjsonEncoder {
|
|
|
3027
3121
|
keys = g_thread_parser.encoded_keys = new (std::nothrow) EncodedKey[ENCODED_KEY_CACHE_SIZE];
|
|
3028
3122
|
}
|
|
3029
3123
|
int rc = 0;
|
|
3124
|
+
#ifdef Py_GIL_DISABLED
|
|
3125
|
+
if (!(opts & (OPT_NON_STR_KEYS | OPT_SORT_KEYS))) {
|
|
3126
|
+
rc = encode_entries_locked(o);
|
|
3127
|
+
if (rc == 0) {
|
|
3128
|
+
close('}');
|
|
3129
|
+
}
|
|
3130
|
+
return rc;
|
|
3131
|
+
}
|
|
3132
|
+
#endif
|
|
3030
3133
|
// Without borrowed (Python code may run, or another thread), a snapshot
|
|
3031
3134
|
// holds the keys and values.
|
|
3032
3135
|
PyObject *items = borrowed ? nullptr : PyDict_Items(o);
|
|
@@ -3168,6 +3271,27 @@ struct OrjsonEncoder {
|
|
|
3168
3271
|
}
|
|
3169
3272
|
};
|
|
3170
3273
|
|
|
3274
|
+
// The flags of option= (None, or an int of orjson's OPT_* flags), as
|
|
3275
|
+
// orjson.dumps checks them.
|
|
3276
|
+
bool orjson_options(PyObject *option, unsigned long *opts) {
|
|
3277
|
+
*opts = 0;
|
|
3278
|
+
if (option == nullptr || option == Py_None) {
|
|
3279
|
+
return true;
|
|
3280
|
+
}
|
|
3281
|
+
if (!PyLong_Check(option)) {
|
|
3282
|
+
PyErr_SetString(PyExc_TypeError, "Invalid opts");
|
|
3283
|
+
return false;
|
|
3284
|
+
}
|
|
3285
|
+
long v = PyLong_AsLong(option);
|
|
3286
|
+
if ((v == -1 && PyErr_Occurred()) || v < 0 || (unsigned long)v > OPT_ALL) {
|
|
3287
|
+
PyErr_Clear();
|
|
3288
|
+
PyErr_SetString(PyExc_TypeError, "Invalid opts");
|
|
3289
|
+
return false;
|
|
3290
|
+
}
|
|
3291
|
+
*opts = (unsigned long)v;
|
|
3292
|
+
return true;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3171
3295
|
// dumpb(obj, /, default=None, option=None), as orjson.dumps.
|
|
3172
3296
|
PyObject *dumpb(PyObject *, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) {
|
|
3173
3297
|
PyObject *obj = nullptr, *dflt = nullptr, *option = nullptr;
|
|
@@ -3196,18 +3320,8 @@ PyObject *dumpb(PyObject *, PyObject *const *args, Py_ssize_t nargs, PyObject *k
|
|
|
3196
3320
|
// fails, and becomes the __cause__ of the error.
|
|
3197
3321
|
e.default_fn = dflt;
|
|
3198
3322
|
}
|
|
3199
|
-
if (option
|
|
3200
|
-
|
|
3201
|
-
PyErr_SetString(PyExc_TypeError, "Invalid opts");
|
|
3202
|
-
return nullptr;
|
|
3203
|
-
}
|
|
3204
|
-
long v = PyLong_AsLong(option);
|
|
3205
|
-
if ((v == -1 && PyErr_Occurred()) || v < 0 || (unsigned long)v > OPT_ALL) {
|
|
3206
|
-
PyErr_Clear();
|
|
3207
|
-
PyErr_SetString(PyExc_TypeError, "Invalid opts");
|
|
3208
|
-
return nullptr;
|
|
3209
|
-
}
|
|
3210
|
-
e.opts = (unsigned long)v;
|
|
3323
|
+
if (!orjson_options(option, &e.opts)) {
|
|
3324
|
+
return nullptr;
|
|
3211
3325
|
}
|
|
3212
3326
|
int rc;
|
|
3213
3327
|
try {
|
|
@@ -3848,7 +3962,16 @@ StreamObject *stream_over(StreamObject *whole, size_t from, size_t to, bool chun
|
|
|
3848
3962
|
return s;
|
|
3849
3963
|
}
|
|
3850
3964
|
|
|
3851
|
-
|
|
3965
|
+
// Work that pool workers can help with: work() takes pieces of it until
|
|
3966
|
+
// none is left. active counts the workers inside work(), guarded by the
|
|
3967
|
+
// pool's mutex.
|
|
3968
|
+
struct PoolTask {
|
|
3969
|
+
size_t active = 0;
|
|
3970
|
+
virtual void work() = 0;
|
|
3971
|
+
virtual ~PoolTask() = default;
|
|
3972
|
+
};
|
|
3973
|
+
|
|
3974
|
+
struct ParallelRun : PoolTask {
|
|
3852
3975
|
StreamObject *whole;
|
|
3853
3976
|
PyObject *fn; // borrowed, or nullptr
|
|
3854
3977
|
std::vector<size_t> bounds; // chunk i is [bounds[i], bounds[i + 1])
|
|
@@ -3861,7 +3984,6 @@ struct ParallelRun {
|
|
|
3861
3984
|
std::vector<Chunk> chunks;
|
|
3862
3985
|
std::atomic<size_t> next{0};
|
|
3863
3986
|
std::atomic<size_t> first_failure{SIZE_MAX};
|
|
3864
|
-
size_t active = 0; // pool workers in work(), guarded by the pool's mutex
|
|
3865
3987
|
|
|
3866
3988
|
void note_failure(size_t i) {
|
|
3867
3989
|
size_t f = first_failure.load();
|
|
@@ -3928,7 +4050,7 @@ struct ParallelRun {
|
|
|
3928
4050
|
|
|
3929
4051
|
// Takes chunks until none is left, letting the garbage collector of
|
|
3930
4052
|
// other threads run between chunks.
|
|
3931
|
-
void work() {
|
|
4053
|
+
void work() override {
|
|
3932
4054
|
for (;;) {
|
|
3933
4055
|
size_t i = next.fetch_add(1);
|
|
3934
4056
|
if (i >= chunks.size() || i > first_failure.load()) {
|
|
@@ -3953,7 +4075,7 @@ struct WorkerPool {
|
|
|
3953
4075
|
std::mutex mu;
|
|
3954
4076
|
std::condition_variable wake; // tasks, or stopping
|
|
3955
4077
|
std::condition_variable done; // a worker finished a task, or exited
|
|
3956
|
-
std::deque<
|
|
4078
|
+
std::deque<PoolTask *> tasks;
|
|
3957
4079
|
PyInterpreterState *interp = nullptr;
|
|
3958
4080
|
size_t workers = 0;
|
|
3959
4081
|
size_t ready = 0; // workers with a thread state, waiting for tasks
|
|
@@ -3971,7 +4093,7 @@ void pool_worker(WorkerPool *pool) {
|
|
|
3971
4093
|
}
|
|
3972
4094
|
pool->done.notify_all();
|
|
3973
4095
|
for (;;) {
|
|
3974
|
-
|
|
4096
|
+
PoolTask *run;
|
|
3975
4097
|
{
|
|
3976
4098
|
std::unique_lock<std::mutex> lk(pool->mu);
|
|
3977
4099
|
pool->wake.wait(lk, [pool] { return pool->stopping || !pool->tasks.empty(); });
|
|
@@ -4072,6 +4194,31 @@ PyObject *stop_pool(PyObject *, PyObject *) {
|
|
|
4072
4194
|
Py_RETURN_NONE;
|
|
4073
4195
|
}
|
|
4074
4196
|
|
|
4197
|
+
// Runs task->work() in the calling thread, helped by up to nthreads - 1
|
|
4198
|
+
// pool workers; returns when the task is done.
|
|
4199
|
+
void run_on_pool(PoolTask *task, size_t nthreads) {
|
|
4200
|
+
WorkerPool *pool = nthreads > 1 ? get_pool(nthreads - 1) : nullptr;
|
|
4201
|
+
if (pool != nullptr) {
|
|
4202
|
+
{
|
|
4203
|
+
std::lock_guard<std::mutex> lk(pool->mu);
|
|
4204
|
+
for (size_t t = 1; t < nthreads; t++) {
|
|
4205
|
+
pool->tasks.push_back(task);
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
pool->wake.notify_all();
|
|
4209
|
+
}
|
|
4210
|
+
task->work(); // the calling thread too
|
|
4211
|
+
if (pool != nullptr) {
|
|
4212
|
+
Py_BEGIN_ALLOW_THREADS
|
|
4213
|
+
std::unique_lock<std::mutex> lk(pool->mu);
|
|
4214
|
+
for (auto it = pool->tasks.begin(); it != pool->tasks.end();) {
|
|
4215
|
+
it = *it == task ? pool->tasks.erase(it) : it + 1;
|
|
4216
|
+
}
|
|
4217
|
+
pool->done.wait(lk, [task] { return task->active == 0; });
|
|
4218
|
+
Py_END_ALLOW_THREADS
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4075
4222
|
#ifndef _WIN32
|
|
4076
4223
|
// A forked child has none of the workers: forget the pool (and its mutexes,
|
|
4077
4224
|
// which another thread may have held during the fork).
|
|
@@ -4120,26 +4267,7 @@ PyObject *run_stream(StreamObject *whole, PyObject *fn, size_t threads) {
|
|
|
4120
4267
|
run.chunks.resize(run.bounds.size() - 1);
|
|
4121
4268
|
// Starting a thread costs about as much as building 512 KB of documents.
|
|
4122
4269
|
size_t nthreads = std::min({threads, run.chunks.size(), std::max<size_t>(whole->len >> 19, 1)});
|
|
4123
|
-
|
|
4124
|
-
if (pool != nullptr) {
|
|
4125
|
-
{
|
|
4126
|
-
std::lock_guard<std::mutex> lk(pool->mu);
|
|
4127
|
-
for (size_t t = 1; t < nthreads; t++) {
|
|
4128
|
-
pool->tasks.push_back(&run);
|
|
4129
|
-
}
|
|
4130
|
-
}
|
|
4131
|
-
pool->wake.notify_all();
|
|
4132
|
-
}
|
|
4133
|
-
run.work(); // the calling thread too
|
|
4134
|
-
if (pool != nullptr) {
|
|
4135
|
-
Py_BEGIN_ALLOW_THREADS
|
|
4136
|
-
std::unique_lock<std::mutex> lk(pool->mu);
|
|
4137
|
-
for (auto it = pool->tasks.begin(); it != pool->tasks.end();) {
|
|
4138
|
-
it = *it == &run ? pool->tasks.erase(it) : it + 1;
|
|
4139
|
-
}
|
|
4140
|
-
pool->done.wait(lk, [&run] { return run.active == 0; });
|
|
4141
|
-
Py_END_ALLOW_THREADS
|
|
4142
|
-
}
|
|
4270
|
+
run_on_pool(&run, nthreads);
|
|
4143
4271
|
size_t f = run.first_failure.load();
|
|
4144
4272
|
size_t total = 0;
|
|
4145
4273
|
for (size_t i = 0; i < run.chunks.size() && i <= f; i++) {
|
|
@@ -4387,6 +4515,213 @@ PyObject *map_many(PyObject *, PyObject *args, PyObject *kwargs) {
|
|
|
4387
4515
|
return r;
|
|
4388
4516
|
}
|
|
4389
4517
|
|
|
4518
|
+
// ---------------------------------------------------------------------------
|
|
4519
|
+
// dumpb_many(records, /, default=None, option=None, *, threads=None): the
|
|
4520
|
+
// records as NDJSON, each as dumpb writes it, followed by a newline. With
|
|
4521
|
+
// threads (free-threaded Python), chunks of records are encoded at once.
|
|
4522
|
+
// ---------------------------------------------------------------------------
|
|
4523
|
+
|
|
4524
|
+
// Encodes items [a, b) into e.out, one per line. Returns b, or the index of
|
|
4525
|
+
// the record that failed, with the exception set.
|
|
4526
|
+
size_t encode_lines(OrjsonEncoder &e, PyObject *const *items, size_t a, size_t b) {
|
|
4527
|
+
for (size_t i = a; i < b; i++) {
|
|
4528
|
+
size_t mark = e.out.len;
|
|
4529
|
+
e.depth = e.default_calls = 0;
|
|
4530
|
+
#ifndef Py_GIL_DISABLED
|
|
4531
|
+
e.borrowed = true;
|
|
4532
|
+
#endif
|
|
4533
|
+
int rc = e.encode(items[i]);
|
|
4534
|
+
if (rc < 0 && e.restart) { // Python code may run: again, without borrowing
|
|
4535
|
+
e.borrowed = e.restart = false;
|
|
4536
|
+
e.out.len = mark;
|
|
4537
|
+
e.depth = e.default_calls = 0;
|
|
4538
|
+
rc = e.encode(items[i]);
|
|
4539
|
+
}
|
|
4540
|
+
if (rc < 0) {
|
|
4541
|
+
return i;
|
|
4542
|
+
}
|
|
4543
|
+
e.out.push_back('\n');
|
|
4544
|
+
}
|
|
4545
|
+
return b;
|
|
4546
|
+
}
|
|
4547
|
+
|
|
4548
|
+
#ifdef Py_GIL_DISABLED
|
|
4549
|
+
struct DumpRun : PoolTask {
|
|
4550
|
+
PyObject *const *items;
|
|
4551
|
+
PyObject *dflt; // borrowed, or nullptr
|
|
4552
|
+
unsigned long opts;
|
|
4553
|
+
std::vector<size_t> bounds; // chunk i is items [bounds[i], bounds[i + 1])
|
|
4554
|
+
struct Chunk {
|
|
4555
|
+
std::unique_ptr<OrjsonEncoder> e; // its output
|
|
4556
|
+
PyObject *type = nullptr, *value = nullptr, *tb = nullptr; // its error
|
|
4557
|
+
};
|
|
4558
|
+
std::vector<Chunk> chunks;
|
|
4559
|
+
std::atomic<size_t> next{0};
|
|
4560
|
+
std::atomic<size_t> first_failure{SIZE_MAX};
|
|
4561
|
+
// Output of the chunks done so far, to size the buffers of the next ones
|
|
4562
|
+
// (growing a buffer copies it).
|
|
4563
|
+
std::atomic<size_t> done_bytes{0};
|
|
4564
|
+
std::atomic<size_t> done_records{0};
|
|
4565
|
+
|
|
4566
|
+
// On an attached thread.
|
|
4567
|
+
void run_chunk(size_t i) {
|
|
4568
|
+
Chunk &c = chunks[i];
|
|
4569
|
+
bool failed;
|
|
4570
|
+
try {
|
|
4571
|
+
c.e.reset(new OrjsonEncoder()); // uses this thread's key cache
|
|
4572
|
+
c.e->default_fn = dflt;
|
|
4573
|
+
c.e->opts = opts;
|
|
4574
|
+
size_t records = bounds[i + 1] - bounds[i];
|
|
4575
|
+
size_t seen = done_records.load(std::memory_order_relaxed);
|
|
4576
|
+
if (seen > 0) {
|
|
4577
|
+
size_t per_record = done_bytes.load(std::memory_order_relaxed) / seen + 1;
|
|
4578
|
+
c.e->out.reserve(per_record * records + per_record * records / 8);
|
|
4579
|
+
}
|
|
4580
|
+
failed = encode_lines(*c.e, items, bounds[i], bounds[i + 1]) < bounds[i + 1];
|
|
4581
|
+
if (!failed) {
|
|
4582
|
+
done_bytes.fetch_add(c.e->out.len, std::memory_order_relaxed);
|
|
4583
|
+
done_records.fetch_add(records, std::memory_order_relaxed);
|
|
4584
|
+
}
|
|
4585
|
+
} catch (const std::bad_alloc &) {
|
|
4586
|
+
PyErr_NoMemory();
|
|
4587
|
+
failed = true;
|
|
4588
|
+
}
|
|
4589
|
+
if (failed) {
|
|
4590
|
+
PyErr_Fetch(&c.type, &c.value, &c.tb);
|
|
4591
|
+
size_t f = first_failure.load();
|
|
4592
|
+
while (i < f && !first_failure.compare_exchange_weak(f, i)) {
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
void work() override {
|
|
4598
|
+
for (;;) {
|
|
4599
|
+
size_t i = next.fetch_add(1);
|
|
4600
|
+
if (i >= chunks.size() || i > first_failure.load()) {
|
|
4601
|
+
return;
|
|
4602
|
+
}
|
|
4603
|
+
run_chunk(i);
|
|
4604
|
+
Py_BEGIN_ALLOW_THREADS
|
|
4605
|
+
Py_END_ALLOW_THREADS
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
};
|
|
4609
|
+
|
|
4610
|
+
// Copies the output of each chunk into place, on several threads too (one
|
|
4611
|
+
// thread copies about 10 GB/s).
|
|
4612
|
+
struct CopyRun : PoolTask {
|
|
4613
|
+
DumpRun *run;
|
|
4614
|
+
char *dst;
|
|
4615
|
+
std::vector<size_t> offsets;
|
|
4616
|
+
std::atomic<size_t> next{0};
|
|
4617
|
+
|
|
4618
|
+
void work() override {
|
|
4619
|
+
for (;;) {
|
|
4620
|
+
size_t i = next.fetch_add(1);
|
|
4621
|
+
if (i >= run->chunks.size()) {
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4624
|
+
const OutBuf &o = run->chunks[i].e->out;
|
|
4625
|
+
memcpy(dst + offsets[i], o.buf, o.len);
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
};
|
|
4629
|
+
|
|
4630
|
+
// The records on several threads, or nullptr with an exception.
|
|
4631
|
+
PyObject *dumpb_many_threads(PyObject *const *items, size_t n, PyObject *dflt,
|
|
4632
|
+
unsigned long opts, size_t threads) {
|
|
4633
|
+
DumpRun run;
|
|
4634
|
+
run.items = items;
|
|
4635
|
+
run.dflt = dflt;
|
|
4636
|
+
run.opts = opts;
|
|
4637
|
+
size_t nchunks = std::min(threads * 8, n);
|
|
4638
|
+
for (size_t k = 0; k <= nchunks; k++) {
|
|
4639
|
+
run.bounds.push_back(n * k / nchunks);
|
|
4640
|
+
}
|
|
4641
|
+
run.chunks.resize(nchunks);
|
|
4642
|
+
run_on_pool(&run, std::min(threads, nchunks));
|
|
4643
|
+
size_t f = run.first_failure.load();
|
|
4644
|
+
PyObject *result = nullptr;
|
|
4645
|
+
if (f == SIZE_MAX) {
|
|
4646
|
+
CopyRun copy;
|
|
4647
|
+
copy.run = &run;
|
|
4648
|
+
size_t total = 0;
|
|
4649
|
+
for (auto &c : run.chunks) {
|
|
4650
|
+
copy.offsets.push_back(total);
|
|
4651
|
+
total += c.e->out.len;
|
|
4652
|
+
}
|
|
4653
|
+
result = PyBytes_FromStringAndSize(nullptr, Py_ssize_t(total));
|
|
4654
|
+
if (result != nullptr) {
|
|
4655
|
+
copy.dst = PyBytes_AS_STRING(result);
|
|
4656
|
+
run_on_pool(©, std::min(threads, nchunks));
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
// The error of the first record that failed, as without threads.
|
|
4660
|
+
for (size_t i = 0; i < run.chunks.size(); i++) {
|
|
4661
|
+
DumpRun::Chunk &c = run.chunks[i];
|
|
4662
|
+
if (i == f) {
|
|
4663
|
+
PyErr_Restore(c.type, c.value, c.tb);
|
|
4664
|
+
} else {
|
|
4665
|
+
Py_XDECREF(c.type);
|
|
4666
|
+
Py_XDECREF(c.value);
|
|
4667
|
+
Py_XDECREF(c.tb);
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
return result;
|
|
4671
|
+
}
|
|
4672
|
+
#endif
|
|
4673
|
+
|
|
4674
|
+
PyObject *dumpb_many(PyObject *, PyObject *args, PyObject *kwargs) {
|
|
4675
|
+
Py_ssize_t threads;
|
|
4676
|
+
bool given;
|
|
4677
|
+
PyObject *rest = take_threads(kwargs, &threads, &given);
|
|
4678
|
+
if (rest == nullptr && PyErr_Occurred()) {
|
|
4679
|
+
return nullptr;
|
|
4680
|
+
}
|
|
4681
|
+
static const char *kwlist[] = {"", "default", "option", nullptr};
|
|
4682
|
+
PyObject *records, *dflt = nullptr, *option = nullptr;
|
|
4683
|
+
int ok = PyArg_ParseTupleAndKeywords(args, rest, "O|OO:dumpb_many",
|
|
4684
|
+
const_cast<char **>(kwlist), &records, &dflt, &option);
|
|
4685
|
+
Py_XDECREF(rest);
|
|
4686
|
+
unsigned long opts;
|
|
4687
|
+
if (!ok || !orjson_options(option, &opts)) {
|
|
4688
|
+
return nullptr;
|
|
4689
|
+
}
|
|
4690
|
+
if (opts & OPT_INDENT_2) {
|
|
4691
|
+
PyErr_SetString(PyExc_ValueError, "dumpb_many writes one record per line: OPT_INDENT_2 is not supported");
|
|
4692
|
+
return nullptr;
|
|
4693
|
+
}
|
|
4694
|
+
// A snapshot of the records (a list could change while we encode it).
|
|
4695
|
+
PyObject *tuple = PySequence_Tuple(records);
|
|
4696
|
+
if (tuple == nullptr) {
|
|
4697
|
+
return nullptr;
|
|
4698
|
+
}
|
|
4699
|
+
PyObject *const *items = &PyTuple_GET_ITEM(tuple, 0);
|
|
4700
|
+
size_t n = size_t(PyTuple_GET_SIZE(tuple));
|
|
4701
|
+
PyObject *result = nullptr;
|
|
4702
|
+
#ifdef Py_GIL_DISABLED
|
|
4703
|
+
if (given && threads > 1 && n > 1) {
|
|
4704
|
+
result = dumpb_many_threads(items, n, dflt, opts, size_t(threads));
|
|
4705
|
+
Py_DECREF(tuple);
|
|
4706
|
+
return result;
|
|
4707
|
+
}
|
|
4708
|
+
#else
|
|
4709
|
+
(void)given;
|
|
4710
|
+
#endif
|
|
4711
|
+
OrjsonEncoder e;
|
|
4712
|
+
e.default_fn = dflt;
|
|
4713
|
+
e.opts = opts;
|
|
4714
|
+
try {
|
|
4715
|
+
if (encode_lines(e, items, 0, n) == n) {
|
|
4716
|
+
result = e.out.take_bytes();
|
|
4717
|
+
}
|
|
4718
|
+
} catch (const std::bad_alloc &) {
|
|
4719
|
+
PyErr_NoMemory();
|
|
4720
|
+
}
|
|
4721
|
+
Py_DECREF(tuple);
|
|
4722
|
+
return result;
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4390
4725
|
// Benchmarking helper: run simdjson without building Python objects.
|
|
4391
4726
|
PyObject *parse_only(PyObject *, PyObject *arg) {
|
|
4392
4727
|
if (!PyBytes_Check(arg)) {
|
|
@@ -4433,9 +4768,15 @@ PyMethodDef methods[] = {
|
|
|
4433
4768
|
#ifdef Py_GIL_DISABLED
|
|
4434
4769
|
{"_stop_pool", stop_pool, METH_NOARGS, "Stop the worker threads (at exit)."},
|
|
4435
4770
|
#endif
|
|
4771
|
+
{"dumpb_many", reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)(void)>(dumpb_many)),
|
|
4772
|
+
METH_VARARGS | METH_KEYWORDS,
|
|
4773
|
+
"dumpb_many(records, /, default=None, option=None, *, threads=None)\n--\n\n"
|
|
4774
|
+
"The records as NDJSON bytes: each as dumpb writes it, then a newline.\n"
|
|
4775
|
+
"With threads (free-threaded Python), the records are encoded on several threads."},
|
|
4436
4776
|
{"map_many", reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)(void)>(map_many)),
|
|
4437
4777
|
METH_VARARGS | METH_KEYWORDS,
|
|
4438
|
-
"map_many(fn, data, *, format, batch_size, threads, lazy): the list of
|
|
4778
|
+
"map_many(fn, data, *, format, batch_size, threads, lazy, fields, default): the list of "
|
|
4779
|
+
"fn(doc) for the "
|
|
4439
4780
|
"documents of data, with fn called in the threads."},
|
|
4440
4781
|
{"parse", parse, METH_O,
|
|
4441
4782
|
"Parse JSON lazily: objects and arrays are returned as read-only views "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastsimdjson
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -42,7 +42,8 @@ function is faster still. It also reads streams of documents (NDJSON, JSON
|
|
|
42
42
|
Lines), on many threads with free-threaded Python, and can extract only the
|
|
43
43
|
fields you need from each document. It writes JSON in two ways: `dumps` returns the same `str` as
|
|
44
44
|
`json.dumps`, and `dumpb` returns the same `bytes` as `orjson.dumps`, as fast
|
|
45
|
-
as orjson
|
|
45
|
+
as orjson; `dumpb_many` writes NDJSON, on many threads with free-threaded
|
|
46
|
+
Python.
|
|
46
47
|
|
|
47
48
|
With pip:
|
|
48
49
|
|
|
@@ -158,6 +159,30 @@ serialize dataclasses, `datetime`, `date`, `time`, `UUID`, numpy arrays or
|
|
|
158
159
|
`OPT_PASSTHROUGH_DATACLASS` and `OPT_PASSTHROUGH_DATETIME` were set, and the
|
|
159
160
|
options that concern only these types are accepted and have no effect.
|
|
160
161
|
|
|
162
|
+
### `dumpb_many`: NDJSON, on several threads
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
data = fastsimdjson.dumpb_many(records) # b'{...}\n{...}\n'
|
|
166
|
+
data = fastsimdjson.dumpb_many(records, threads=16) # free-threaded Python
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`dumpb_many(records, default=None, option=None, *, threads=None)` returns
|
|
170
|
+
the records (a list, a tuple or any iterable) as NDJSON `bytes`: each record
|
|
171
|
+
as `dumpb` writes it, followed by a newline, so that the output reads back
|
|
172
|
+
with `loads_many(data, format="lines")`. `default` and `option` are those of
|
|
173
|
+
`dumpb`, except `OPT_INDENT_2`, which would spread a record over several
|
|
174
|
+
lines (`ValueError`); `OPT_APPEND_NEWLINE` changes nothing.
|
|
175
|
+
|
|
176
|
+
On free-threaded Python, `threads=N` writes chunks of records on N threads at
|
|
177
|
+
once (`threads=0` or `None`: one per processor), with the worker threads of
|
|
178
|
+
`map_many`. The output and the errors are those of one thread: when records
|
|
179
|
+
cannot be serialized, the error of the first of them is raised (`default` may
|
|
180
|
+
then have been called on later records, in other threads, and it must be
|
|
181
|
+
safe to call from several threads at once). With the GIL, the calling thread
|
|
182
|
+
does all the work. Like `dumpb`, `dumpb_many` does not crash when other
|
|
183
|
+
threads change the records meanwhile, but the output then reflects some of
|
|
184
|
+
the changes and not others.
|
|
185
|
+
|
|
161
186
|
### Files
|
|
162
187
|
|
|
163
188
|
```python
|
|
@@ -380,7 +405,7 @@ geometric mean over the 22 files (higher is better).
|
|
|
380
405
|
|
|
381
406
|
<picture>
|
|
382
407
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads_dark.png">
|
|
383
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
408
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, jiter without its string cache 0.44, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
|
|
384
409
|
</picture>
|
|
385
410
|
|
|
386
411
|
| parser | GB/s | vs `json.loads` |
|
|
@@ -391,7 +416,8 @@ geometric mean over the 22 files (higher is better).
|
|
|
391
416
|
| ujson 6.0.0 | 0.37 | 1.66× |
|
|
392
417
|
| cysimdjson 26.27 | 0.43 | 1.96× |
|
|
393
418
|
| pysimdjson 7.0.2 | 0.44 | 1.98× |
|
|
394
|
-
| jiter 0.17.0 | 0.
|
|
419
|
+
| jiter 0.17.0 (no string cache) | 0.44 | 2.00× |
|
|
420
|
+
| jiter 0.17.0 (string cache, the default) | 0.48 | 2.21× |
|
|
395
421
|
| msgspec 0.22.0 | 0.53 | 2.42× |
|
|
396
422
|
| orjson 3.12.0 | 0.60 | 2.74× |
|
|
397
423
|
| **fastsimdjson `loads`** | **0.78** | **3.53×** |
|
|
@@ -407,7 +433,7 @@ the collector enabled and 1.22× with it disabled (geometric means); without
|
|
|
407
433
|
the collector, `canada.json`, `mesh.json` and `numbers.json` are ties. yyjson 4.0.6 is left out: it
|
|
408
434
|
returns wrong strings for non-ASCII text. jiter (the parser of pydantic)
|
|
409
435
|
caches strings by default; turning the cache off (`cache_mode=False`) makes it
|
|
410
|
-
slower here
|
|
436
|
+
slower here.
|
|
411
437
|
|
|
412
438
|
Parsing is no longer the bottleneck. simdjson alone parses these files at
|
|
413
439
|
3.1 GB/s. It accounts for about a third of the time of `loads`; the rest goes
|
|
@@ -422,12 +448,14 @@ and the screen name of the 100 statuses of `twitter.json`:
|
|
|
422
448
|
|
|
423
449
|
<picture>
|
|
424
450
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_parse_dark.png">
|
|
425
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_parse.png" width="80%" alt="Reading part of twitter.json: fastsimdjson parse 158 µs, pysimdjson 179, cysimdjson 229, msgspec 336, fastsimdjson loads 861, orjson 1009, json 3922">
|
|
451
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_parse.png" width="80%" alt="Reading part of twitter.json: fastsimdjson parse 158 µs, pysimdjson 179, cysimdjson 229, msgspec 336, fastsimdjson loads 861, orjson 1009, jiter 1281, jiter without its string cache 1592, json 3922">
|
|
426
452
|
</picture>
|
|
427
453
|
|
|
428
454
|
| method | µs |
|
|
429
455
|
|---|---:|
|
|
430
456
|
| `json.loads` | 3922 |
|
|
457
|
+
| jiter (no string cache) | 1592 |
|
|
458
|
+
| jiter (string cache) | 1281 |
|
|
431
459
|
| orjson | 1009 |
|
|
432
460
|
| fastsimdjson `loads` | 861 |
|
|
433
461
|
| msgspec (typed `Struct`) | 336 |
|
|
@@ -481,6 +509,18 @@ times faster than msgspec and 10 times faster than `json`. The comparison was
|
|
|
481
509
|
run on a processor with AVX-512, which `dumpb` and orjson both use to escape
|
|
482
510
|
strings; other x64 processors use SSE2 and ARM processors NEON.
|
|
483
511
|
|
|
512
|
+
### Writing NDJSON with `dumpb_many`
|
|
513
|
+
|
|
514
|
+
The 593,945 events of two hours of [GH Archive](https://www.gharchive.org/)
|
|
515
|
+
(1.87 GB of NDJSON), written back as NDJSON from Python objects. With the
|
|
516
|
+
GIL (Python 3.14.6), one thread: `orjson.dumps` on each record, joined, runs
|
|
517
|
+
at 0.94 GB/s, `dumpb` on each record at 1.07 GB/s, and `dumpb_many` at
|
|
518
|
+
1.41 GB/s. On free-threaded Python 3.14.6t, GB/s:
|
|
519
|
+
|
|
520
|
+
| threads | 1 | 8 | 32 | 64 |
|
|
521
|
+
|---|---:|---:|---:|---:|
|
|
522
|
+
| `dumpb_many(records, threads=N)` | 1.3 | 6.0 | 11 | 12 |
|
|
523
|
+
|
|
484
524
|
### Streams with `loads_many`
|
|
485
525
|
|
|
486
526
|
20 MB of NDJSON (5268 objects and arrays, one per line), made from the same
|
|
@@ -488,12 +528,14 @@ files; best of three runs:
|
|
|
488
528
|
|
|
489
529
|
<picture>
|
|
490
530
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_ndjson_dark.png">
|
|
491
|
-
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_ndjson.png" width="80%" alt="Reading NDJSON: fastsimdjson parse_many 0.94 GB/s, loads_many 0.38, msgspec decode_lines 0.37, fastsimdjson loads per line 0.33, msgspec decode per line 0.27, orjson per line 0.26, json per line 0.12">
|
|
531
|
+
<img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_ndjson.png" width="80%" alt="Reading NDJSON: fastsimdjson parse_many 0.94 GB/s, loads_many 0.38, msgspec decode_lines 0.37, fastsimdjson loads per line 0.33, msgspec decode per line 0.27, orjson per line 0.26, jiter per line 0.22, jiter without its string cache per line 0.20, json per line 0.12">
|
|
492
532
|
</picture>
|
|
493
533
|
|
|
494
534
|
| method | ms | GB/s |
|
|
495
535
|
|---|---:|---:|
|
|
496
536
|
| `json.loads` on each line | 169 | 0.12 |
|
|
537
|
+
| jiter on each line (no string cache) | 98 | 0.20 |
|
|
538
|
+
| jiter on each line (string cache) | 90 | 0.22 |
|
|
497
539
|
| orjson on each line | 77 | 0.26 |
|
|
498
540
|
| msgspec `decode` on each line | 74 | 0.27 |
|
|
499
541
|
| fastsimdjson `loads` on each line | 60 | 0.33 |
|
|
@@ -435,3 +435,115 @@ def test_thread_exit_releases_caches():
|
|
|
435
435
|
t.start()
|
|
436
436
|
t.join()
|
|
437
437
|
assert sys.getrefcount(key) == before
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# dumpb_many: each record as dumpb writes it, then a newline.
|
|
441
|
+
|
|
442
|
+
def _lines(records, **kw):
|
|
443
|
+
return b"".join(fastsimdjson.dumpb(r, **kw) + b"\n" for r in records)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _error(fn):
|
|
447
|
+
try:
|
|
448
|
+
fn()
|
|
449
|
+
except Exception as e: # noqa: BLE001
|
|
450
|
+
return type(e), str(e), type(e.__cause__)
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@pytest.mark.parametrize("seed", range(10))
|
|
455
|
+
def test_dumpb_many_random(seed):
|
|
456
|
+
rnd = random.Random(seed)
|
|
457
|
+
records = [random_value(rnd) for _ in range(rnd.randrange(1, 400))]
|
|
458
|
+
expected = _lines(records)
|
|
459
|
+
for threads in (None, 1, 4, 7):
|
|
460
|
+
kw = {} if threads is None else {"threads": threads}
|
|
461
|
+
assert fastsimdjson.dumpb_many(records, **kw) == expected
|
|
462
|
+
assert fastsimdjson.dumpb_many(tuple(records), **kw) == expected
|
|
463
|
+
assert fastsimdjson.dumpb_many(iter(records), **kw) == expected
|
|
464
|
+
# The output is NDJSON that reads back.
|
|
465
|
+
assert list(fastsimdjson.loads_many(expected, format="lines")) == \
|
|
466
|
+
[fastsimdjson.loads(fastsimdjson.dumpb(r)) for r in records]
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def test_dumpb_many_options():
|
|
470
|
+
records = [{"b": 1, "a": {1: 2}}, {"z": [3]}] * 50
|
|
471
|
+
for option in (4 | 32, 32): # OPT_NON_STR_KEYS | OPT_SORT_KEYS; OPT_SORT_KEYS
|
|
472
|
+
expected = _error(lambda: _lines(records, option=option)) or _lines(records, option=option)
|
|
473
|
+
for threads in (1, 4):
|
|
474
|
+
got = _error(lambda: fastsimdjson.dumpb_many(records, option=option, threads=threads)) or \
|
|
475
|
+
fastsimdjson.dumpb_many(records, None, option, threads=threads)
|
|
476
|
+
assert got == expected
|
|
477
|
+
# One newline per record, with or without OPT_APPEND_NEWLINE.
|
|
478
|
+
assert fastsimdjson.dumpb_many([1, 2], option=fastsimdjson.OPT_APPEND_NEWLINE) == b"1\n2\n"
|
|
479
|
+
assert fastsimdjson.dumpb_many([]) == b""
|
|
480
|
+
assert fastsimdjson.dumpb_many([], threads=4) == b""
|
|
481
|
+
with pytest.raises(ValueError):
|
|
482
|
+
fastsimdjson.dumpb_many([1], option=fastsimdjson.OPT_INDENT_2)
|
|
483
|
+
with pytest.raises(TypeError, match="Invalid opts"):
|
|
484
|
+
fastsimdjson.dumpb_many([1], option="x")
|
|
485
|
+
with pytest.raises(TypeError):
|
|
486
|
+
fastsimdjson.dumpb_many(5)
|
|
487
|
+
with pytest.raises(TypeError):
|
|
488
|
+
fastsimdjson.dumpb_many()
|
|
489
|
+
with pytest.raises(ValueError):
|
|
490
|
+
fastsimdjson.dumpb_many([1], threads=-1)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class _Thing:
|
|
494
|
+
def __init__(self, n):
|
|
495
|
+
self.n = n
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_dumpb_many_default_and_errors():
|
|
499
|
+
records = [{"i": i, "t": _Thing(i)} for i in range(3000)]
|
|
500
|
+
|
|
501
|
+
def default(o):
|
|
502
|
+
if isinstance(o, _Thing):
|
|
503
|
+
if o.n in (1700, 2500):
|
|
504
|
+
raise ValueError(f"bad {o.n}")
|
|
505
|
+
return o.n * 2
|
|
506
|
+
raise TypeError
|
|
507
|
+
|
|
508
|
+
good = records[:1700]
|
|
509
|
+
for threads in (None, 1, 4, 16):
|
|
510
|
+
kw = {} if threads is None else {"threads": threads}
|
|
511
|
+
assert fastsimdjson.dumpb_many(good, default=default, **kw) == _lines(good, default=default)
|
|
512
|
+
# The error of the first record that fails, as dumpb raises it (with
|
|
513
|
+
# default's exception as its cause).
|
|
514
|
+
got = _error(lambda: fastsimdjson.dumpb_many(records, default, **kw))
|
|
515
|
+
assert got == _error(lambda: fastsimdjson.dumpb(records[1700], default=default))
|
|
516
|
+
assert got[2] is ValueError
|
|
517
|
+
bad = [1, 2, {"x": object()}, 3, [object()]]
|
|
518
|
+
assert _error(lambda: fastsimdjson.dumpb_many(bad, **kw)) == \
|
|
519
|
+
_error(lambda: fastsimdjson.dumpb(bad[2]))
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def test_dumpb_many_under_mutation():
|
|
523
|
+
# Another thread changes the records while they are written: the output
|
|
524
|
+
# is still one valid document per record (free-threaded Python).
|
|
525
|
+
import threading
|
|
526
|
+
|
|
527
|
+
records = [{"i": i, "l": [i, "é", {"k": i}], "d": {"x": [1, 2]}} for i in range(3000)]
|
|
528
|
+
stop = threading.Event()
|
|
529
|
+
|
|
530
|
+
def mutate():
|
|
531
|
+
rnd = random.Random(0)
|
|
532
|
+
while not stop.is_set():
|
|
533
|
+
r = records[rnd.randrange(len(records))]
|
|
534
|
+
r["l"].append(rnd.random())
|
|
535
|
+
if len(r["l"]) > 6:
|
|
536
|
+
r["l"].clear()
|
|
537
|
+
r["d"] = {"y": "é" * rnd.randrange(4), "z": [None]}
|
|
538
|
+
r.pop("i", None) if rnd.random() < 0.5 else r.update(i=rnd.randrange(9))
|
|
539
|
+
|
|
540
|
+
t = threading.Thread(target=mutate)
|
|
541
|
+
t.start()
|
|
542
|
+
try:
|
|
543
|
+
for _ in range(20):
|
|
544
|
+
out = fastsimdjson.dumpb_many(records, threads=8)
|
|
545
|
+
docs = list(fastsimdjson.loads_many(out, format="lines"))
|
|
546
|
+
assert len(docs) == len(records)
|
|
547
|
+
finally:
|
|
548
|
+
stop.set()
|
|
549
|
+
t.join()
|
|
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
|