GH-50944: [C++] Replace RapidJSON with simdjson in JSON chunker - #50945
GH-50944: [C++] Replace RapidJSON with simdjson in JSON chunker#50945Reranko05 wants to merge 22 commits into
Conversation
e9fc9fe to
be4c2a1
Compare
pitrou
left a comment
There was a problem hiding this comment.
I don't understand why this is parsing JSON by hand?
It seems that we might be able to use simdjson::ondemand::parser::iterate_many.
I initially tried using That said, I agree that parsing JSON manually here is not ideal. I'll revisit this using |
|
@pitrou I tried using Manual structural parsing approach preserved the existing test behavior. @rok, since your earlier implementation was helpful here, do you have any suggestions on how to preserve the current error behavior with |
As long as an error is reported while reading the JSON stream, I don't think we care if it's reported by the chunker or the parser. |
|
@pitrou I gave |
Hmm, I see. Thanks for trying anyway :-)
Well, as a last resort, yes. The problem:
|
|
Try to understand the issue. Is it that json strings legal for rapidjson may fail on simdjson, makes future Arrow release potentially incompatible to old version? Writing our own optimized version looks not ideal. Can we just use simdjson? It's state-of-the-art, and even with self written object delimiter, there's still incompatibility risk I'm afraid. |
|
@cyb70289 I don't think the issue is JSON compatibility between RapidJSON and simdjson. The main issue I ran into is the boundary/streaming semantics. The previous RapidJSON implementation uses With I agree that writing our own optimized JSON parser would not be ideal. The manual approach I used, and which @rok also implemented in rok#47, only scans for the boundary of the first complete object or array while respecting strings and escapes, and then lets simdjson perform the actual JSON validation. But I agree this still introduces complexity and needs careful testing. If there is a way to use simdjson directly while preserving the old stop-after-one-value semantics, that would definitely be preferable. |
Is that a problem? We want to keep compatibility when parsing valid JSON streams. The failure mode for an invalid JSON stream can change. |
|
A discussion about ignoring trailing garbage in simdjson. Looks there're real use cases lenient parsing can be useful. |
3c934c5 to
ce19130
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
python/pyarrow/tests/test_json.py:155
- The test now accepts any ValueError, which weakens coverage for the expected failure mode when block_size is too small. Other tests in this file still assert the specific "straddling object ..." error; keeping a message match here helps ensure regressions don’t silently change the error raised for undersized blocks.
with pytest.raises(ValueError):
self.read_bytes(data, read_options=read_options,
parse_options=parse_options)
| pass | ||
| else: | ||
| with pytest.raises(pa.ArrowInvalid): | ||
| reader.read_next_batch() |
There was a problem hiding this comment.
Updated for readahead, since the error may be surfaced by either the chunker or parser.
There was a problem hiding this comment.
Can we use this?
if self.use_threads:
# TODO: Add a comment why the first read_next_batch() may not raise an error with threads.
with pytest.raises(pa.ArrowInvalid, match="JSON (parse|chunk) error"):
reader.read_next_batch()
reader.read_next_batch()
else:
with pytest.raises(pa.ArrowInvalid, match="JSON parse error"):
reader.read_next_batch()| pass | ||
| else: | ||
| with pytest.raises(pa.ArrowInvalid): | ||
| reader.read_next_batch() |
There was a problem hiding this comment.
Can we use this?
if self.use_threads:
# TODO: Add a comment why the first read_next_batch() may not raise an error with threads.
with pytest.raises(pa.ArrowInvalid, match="JSON (parse|chunk) error"):
reader.read_next_batch()
reader.read_next_batch()
else:
with pytest.raises(pa.ArrowInvalid, match="JSON parse error"):
reader.read_next_batch()| // found incomplete object or block is empty | ||
|
|
||
| simdjson::padded_string padded(block); | ||
| simdjson::ondemand::parser parser; |
There was a problem hiding this comment.
Cam we reuse simdjson::ondemand::parser in FindLast() and ConsumeWholeObject() for performance?
There was a problem hiding this comment.
Done. Reused a single simdjson::ondemand::parser in both FindLast() and ConsumeWholeObject().
| if (length == string_view::npos || length == 0) { | ||
| // found incomplete object or block is empty | ||
|
|
||
| simdjson::padded_string padded(block); |
There was a problem hiding this comment.
Can we use simdjson::padded_string_view if possible by introducing our internal buffer?
See also: https://github.com/simdjson/simdjson/blob/master/doc/performance.md#reusing-string-buffers
2b91c84 to
729a6a8
Compare
729a6a8 to
05033bf
Compare
|
|
||
| simdjson::ondemand::document_stream stream; | ||
|
|
||
| if (parser_.iterate_many(GetPaddedStringView(block)).get(stream) != |
There was a problem hiding this comment.
iterate_many rejects strings larger than 1mb. We could:
| if (parser_.iterate_many(GetPaddedStringView(block)).get(stream) != | |
| if (parser_.iterate_many(GetPaddedStringView(block), block.size()).get(stream) != |
There was a problem hiding this comment.
It says that each document must not be larger than 1MB, not that the whole string is limited.
There was a problem hiding this comment.
Also, I don't understand why the suggested fix would change anything?
|
|
||
| simdjson::ondemand::document_stream stream; | ||
|
|
||
| if (parser_.iterate_many(GetPaddedStringView(input)).get(stream) != |
There was a problem hiding this comment.
See comment above
| if (parser_.iterate_many(GetPaddedStringView(input)).get(stream) != | |
| if (parser_.iterate_many(GetPaddedStringView(input), input.size()).get(stream) != |
| } else if (block.empty()) { | ||
| input = simdjson::padded_string(partial); | ||
| } else { | ||
| simdjson::padded_string_builder builder(partial.size() + block.size()); |
There was a problem hiding this comment.
padded_string_builder was introduced in v4.3.0 so builds against 4.0-4.2 will likely fail.
There was a problem hiding this comment.
Pity. Either we can backport it, emulate it, or use #if -based version checks.
Rationale for this change
This PR continues the simdjson migration by replacing the RapidJSON-based JSON boundary detection used by the JSON chunker.
The existing implementation uses RapidJSON's streaming parser to identify complete JSON values. This change replaces that logic with structural boundary detection and simdjson validation.
Changes
simdjson::dom::parser.Fixes: #50944