simdjson 5.0.2
Ridiculously Fast JSON
Loading...
Searching...
No Matches
document_stream-inl.h
1#ifndef SIMDJSON_GENERIC_ONDEMAND_DOCUMENT_STREAM_INL_H
2
3#ifndef SIMDJSON_CONDITIONAL_INCLUDE
4#define SIMDJSON_GENERIC_ONDEMAND_DOCUMENT_STREAM_INL_H
5#include "simdjson/generic/ondemand/base.h"
6#include "simdjson/generic/ondemand/document_stream.h"
7#include "simdjson/generic/ondemand/document-inl.h"
8#include "simdjson/generic/implementation_simdjson_result_base-inl.h"
9#endif // SIMDJSON_CONDITIONAL_INCLUDE
10
11#include <algorithm>
12#include <cstring>
13#include <stdexcept>
14
15namespace simdjson {
16namespace SIMDJSON_IMPLEMENTATION {
17namespace ondemand {
18
19#ifdef SIMDJSON_THREADS_ENABLED
20
21inline void stage1_worker::finish() {
22 // After calling "run" someone would call finish() to wait
23 // for the end of the processing.
24 // This function will wait until either the thread has done
25 // the processing or, else, the destructor has been called.
26 std::unique_lock<std::mutex> lock(locking_mutex);
27 cond_var.wait(lock, [this]{return has_work == false;});
28}
29
30inline stage1_worker::~stage1_worker() {
31 // The thread may never outlive the stage1_worker instance
32 // and will always be stopped/joined before the stage1_worker
33 // instance is gone.
34 stop_thread();
35}
36
37inline void stage1_worker::start_thread() {
38 std::unique_lock<std::mutex> lock(locking_mutex);
39 if(thread.joinable()) {
40 return; // This should never happen but we never want to create more than one thread.
41 }
42 thread = std::thread([this]{
43 while(true) {
44 std::unique_lock<std::mutex> thread_lock(locking_mutex);
45 // We wait for either "run" or "stop_thread" to be called.
46 cond_var.wait(thread_lock, [this]{return has_work || !can_work;});
47 // If, for some reason, the stop_thread() method was called (i.e., the
48 // destructor of stage1_worker is called, then we want to immediately destroy
49 // the thread (and not do any more processing).
50 if(!can_work) {
51 break;
52 }
53 this->owner->stage1_thread_error = this->owner->run_stage1(*this->stage1_thread_parser,
54 this->_next_batch_start);
55 this->has_work = false;
56 // The condition variable call should be moved after thread_lock.unlock() for performance
57 // reasons but thread sanitizers may report it as a data race if we do.
58 // See https://stackoverflow.com/questions/35775501/c-should-condition-variable-be-notified-under-lock
59 cond_var.notify_one(); // will notify "finish"
60 thread_lock.unlock();
61 }
62 }
63 );
64}
65
66
67inline void stage1_worker::stop_thread() {
68 std::unique_lock<std::mutex> lock(locking_mutex);
69 // We have to make sure that all locks can be released.
70 can_work = false;
71 has_work = false;
72 cond_var.notify_all();
73 lock.unlock();
74 if(thread.joinable()) {
75 thread.join();
76 }
77}
78
79inline void stage1_worker::run(document_stream * ds, parser * stage1, size_t next_batch_start) {
80 std::unique_lock<std::mutex> lock(locking_mutex);
81 owner = ds;
82 _next_batch_start = next_batch_start;
83 stage1_thread_parser = stage1;
84 has_work = true;
85 // The condition variable call should be moved after thread_lock.unlock() for performance
86 // reasons but thread sanitizers may report it as a data race if we do.
87 // See https://stackoverflow.com/questions/35775501/c-should-condition-variable-be-notified-under-lock
88 cond_var.notify_one(); // will notify the thread lock that we have work
89 lock.unlock();
90}
91
92#endif // SIMDJSON_THREADS_ENABLED
93
95 ondemand::parser &_parser,
96 const uint8_t *_buf,
97 size_t _len,
98 size_t _batch_size,
99 bool _allow_comma_separated,
100 stream_format _format
101) noexcept
102 : parser{&_parser},
103 buf{_buf},
104 len{_len},
105 batch_size{_batch_size <= MINIMAL_BATCH_SIZE ? MINIMAL_BATCH_SIZE : _batch_size},
106 allow_comma_separated{_allow_comma_separated},
107 format{_format},
108 error{SUCCESS}
109 #ifdef SIMDJSON_THREADS_ENABLED
110 , use_thread(_parser.threaded) // we need to make a copy because _parser.threaded can change
111 #endif
112{
113}
114
115simdjson_inline document_stream::document_stream() noexcept
116 : parser{nullptr},
117 buf{nullptr},
118 len{0},
119 batch_size{0},
120 allow_comma_separated{false},
122 error{UNINITIALIZED}
123 #ifdef SIMDJSON_THREADS_ENABLED
124 , use_thread(false)
125 #endif
126{
127}
128
129simdjson_inline document_stream::~document_stream() noexcept
130{
131 #ifdef SIMDJSON_THREADS_ENABLED
132 worker.reset();
133 #endif
134}
135
136inline size_t document_stream::size_in_bytes() const noexcept {
137 return len;
138}
139
140inline size_t document_stream::truncated_bytes() const noexcept {
141 // Stage 1 returns EMPTY on zero-length input before it writes the index
142 // sentinels read below, so they would still hold a previous stream's values.
143 if (len == 0) { return 0; }
144 if(error == CAPACITY) { return len - batch_start; }
145 return parser->implementation->structural_indexes[parser->implementation->n_structural_indexes] - parser->implementation->structural_indexes[parser->implementation->n_structural_indexes + 1];
146}
147
148simdjson_inline document_stream::iterator::iterator() noexcept
149 : stream{nullptr}, finished{true} {
150}
151
152simdjson_inline document_stream::iterator::iterator(document_stream* _stream, bool is_end) noexcept
153 : stream{_stream}, finished{is_end} {
154}
155
159
161 // If there is an error, then we want the iterator
162 // to be finished, no matter what. (E.g., we do not
163 // keep generating documents with errors, or go beyond
164 // a document with errors.)
165 //
166 // Users do not have to call "operator*()" when they use operator++,
167 // so we need to end the stream in the operator++ function.
168 //
169 // Note that setting finished = true is essential otherwise
170 // we would enter an infinite loop.
171 if (stream->error) { finished = true; }
172 // Note that stream->error() is guarded against error conditions
173 // (it will immediately return if stream->error casts to false).
174 // In effect, this next function does nothing when (stream->error)
175 // is true (hence the risk of an infinite loop).
176 stream->next();
177 // If that was the last document, we're finished.
178 // It is the only type of error we do not want to appear
179 // in operator*.
180 if (stream->error == EMPTY) { finished = true; }
181 // If we had any other kind of error (not EMPTY) then we want
182 // to pass it along to the operator* and we cannot mark the result
183 // as "finished" just yet.
184 return *this;
185}
186
187simdjson_inline bool document_stream::iterator::at_end() const noexcept {
188 return finished;
189}
190
191
192simdjson_inline bool document_stream::iterator::operator!=(const document_stream::iterator &other) const noexcept {
193 return finished != other.finished;
194}
195
196simdjson_inline bool document_stream::iterator::operator==(const document_stream::iterator &other) const noexcept {
197 return finished == other.finished;
198}
199
201 start();
202 // If there are no documents, we're finished.
203 return iterator(this, error == EMPTY);
204}
205
207 return iterator(this, true);
208}
209
210inline void document_stream::start() noexcept {
211 if (error) { return; }
212 error = parser->allocate(batch_size);
213 if (error) { return; }
214 // Always run the first stage 1 parse immediately
215 batch_start = 0;
216 error = run_stage1(*parser, batch_start);
217 while(error == EMPTY) {
218 // In exceptional cases, we may start with an empty block
219 batch_start = next_batch_start();
220 if (batch_start >= len) { return; }
221 error = run_stage1(*parser, batch_start);
222 }
223 if (error) { return; }
224 // For json_sequence mode, structural_indexes[0] points to the actual JSON value
225 // after the RS delimiter and any following whitespace. For regular mode, it is
226 // the offset from batch_start to the first document in the batch.
227 doc_index = batch_start + parser->implementation->structural_indexes[0];
228 doc = document(json_iterator(&buf[batch_start], parser));
229 doc.iter._streaming = true;
230
231 #ifdef SIMDJSON_THREADS_ENABLED
232 if (use_thread && next_batch_start() < len) {
233 // Kick off the first thread on next batch if needed
234 if (worker.get() == nullptr) {
235 worker.reset(new(std::nothrow) stage1_worker());
236 if (worker.get() == nullptr) { error = MEMALLOC; return; }
237 }
238 error = stage1_thread_parser.allocate(batch_size);
239 if (error) { return; }
240 worker->start_thread();
241 start_stage1_thread();
242 if (error) { return; }
243 }
244 #endif // SIMDJSON_THREADS_ENABLED
245}
246
247inline void document_stream::next() noexcept {
248 // We always enter at once once in an error condition.
249 if (error) { return; }
250 next_document();
251 if (error) { return; }
252 auto cur_struct_index = doc.iter._root - parser->implementation->structural_indexes.get();
253 doc_index = batch_start + parser->implementation->structural_indexes[cur_struct_index];
254
255 // Check if at end of structural indexes (i.e. at end of batch)
256 if(cur_struct_index >= static_cast<int64_t>(parser->implementation->n_structural_indexes)) {
257 error = EMPTY;
258 // Load another batch (if available)
259 while (error == EMPTY) {
260 batch_start = next_batch_start();
261 if (batch_start >= len) { break; }
262 #ifdef SIMDJSON_THREADS_ENABLED
263 if(use_thread) {
264 load_from_stage1_thread();
265 } else {
266 error = run_stage1(*parser, batch_start);
267 }
268 #else
269 error = run_stage1(*parser, batch_start);
270 #endif
305 doc.iter = json_iterator(&buf[batch_start], parser);
306 doc.iter._streaming = true;
311 if (error) { continue; } // If the error was EMPTY, we may want to load another batch.
312 doc_index = batch_start + parser->implementation->structural_indexes[0];
313 }
314 }
315}
316
317simdjson_inline uint8_t document_stream::document_delimiter() const noexcept {
318 switch (format) {
319 case stream_format::newline_delimited: return '\n';
320 case stream_format::json_sequence: return 0x1E;
321 default: return 0;
322 }
323}
324
325simdjson_inline bool document_stream::skip_to_delimiter(uint8_t delimiter) noexcept {
326 const uint8_t *const base = &buf[batch_start];
327 const token_position pos = doc.iter.position();
328 const token_position end = doc.iter.end_position();
329 if (pos >= end) { return false; }
330 const size_t here = size_t(doc.iter.token.peek(pos) - base);
331 const size_t batch_len =
332 (len - batch_start < batch_size) ? len - batch_start : batch_size;
333 if (here >= batch_len) { return false; }
334 const uint8_t *const found = static_cast<const uint8_t *>(
335 std::memchr(base + here, delimiter, batch_len - here));
336 if (found == nullptr) { return false; }
337
338 const uint32_t boundary = uint32_t(found - base);
339 // The answer is near `pos`: the delimiter ends the current document, while
340 // `end` spans the whole batch. Gallop first so the cost follows the distance
341 // rather than the size of the batch.
342 token_position lo = pos;
343 size_t hop = 1;
344 while (lo + hop < end && lo[hop] < boundary) { lo += hop; hop <<= 1; }
345 token_position hi = (lo + hop < end) ? lo + hop : end;
346 while (lo < hi) {
347 const token_position mid = lo + ((hi - lo) >> 1);
348 if (*mid < boundary) { lo = mid + 1; } else { hi = mid; }
349 }
350 doc.iter.token.set_position(lo);
351 return true;
352}
353
354inline void document_stream::next_document() noexcept {
355 // A delimiter that cannot occur inside a document tells us where the current
356 // one ends, so we can jump there instead of walking every structural. Only
357 // valid while the iterator is still inside the document: a consumed document
358 // already sits on the next one's first token, and skip_child() returns at
359 // once for it.
360 //
361 // The jump does not structure-validate the unread remainder of the current
362 // document: under newline_delimited / json_sequence the next delimiter is
363 // assumed to be the true document boundary. Callers that leave depth() > 0
364 // while violating that contract (e.g. pretty multi-line JSON under
365 // newline_delimited) can mis-align following documents; use
366 // whitespace_delimited if unsure.
367 const uint8_t delimiter = document_delimiter();
368 if (delimiter != 0 && !error && doc.iter.depth() > 0 &&
369 skip_to_delimiter(delimiter)) {
370 doc.iter._depth = 1;
371 doc.iter._string_buf_loc = parser->string_buf.get();
372 doc.iter._root = doc.iter.position();
373 return;
374 }
375 // Go to next place where depth=0 (document depth)
376 error = doc.iter.skip_child(0);
377 if (error) { return; }
378 // Always set depth=1 at the start of document
379 doc.iter._depth = 1;
380 // consume comma if comma separated is allowed
381 if (allow_comma_separated) {
382 error_code ignored = doc.iter.consume_character(',');
383 static_cast<void>(ignored); // ignored on purpose
384 }
385 // Resets the string buffer at the beginning, thus invalidating the strings.
386 doc.iter._string_buf_loc = parser->string_buf.get();
387 doc.iter._root = doc.iter.position();
388}
389
390inline size_t document_stream::next_batch_start() const noexcept {
391 return batch_start + parser->implementation->structural_indexes[parser->implementation->n_structural_indexes];
392}
393
394inline error_code document_stream::run_stage1(ondemand::parser &p, size_t _batch_start) noexcept {
395 // This code only updates the structural index in the parser, it does not update any json_iterator
396 // instance.
397 size_t remaining = len - _batch_start;
398 stage1_mode mode;
399 if (remaining <= batch_size) {
400 // Final batch
401 switch (format) {
403 mode = stage1_mode::json_sequence_final;
404 break;
406 mode = stage1_mode::comma_delimited_final;
407 break;
408 default:
409 mode = stage1_mode::streaming_final;
410 break;
411 }
412 return p.implementation->stage1(&buf[_batch_start], remaining, mode);
413 } else {
414 // Partial batch
415 switch (format) {
417 mode = stage1_mode::json_sequence_partial;
418 break;
420 mode = stage1_mode::comma_delimited_partial;
421 break;
422 default:
423 mode = stage1_mode::streaming_partial;
424 break;
425 }
426 return p.implementation->stage1(&buf[_batch_start], batch_size, mode);
427 }
428}
429
430simdjson_inline size_t document_stream::iterator::current_index() const noexcept {
431 return stream->doc_index;
432}
433
434simdjson_inline std::string_view document_stream::iterator::source() const noexcept {
435 // On error (e.g., CAPACITY), there is no document to walk: return the rest of
436 // the input, as the DOM document_stream does.
437 if (stream->error) {
438 return std::string_view(reinterpret_cast<const char*>(stream->buf) + current_index(), stream->len - current_index());
439 }
440 // Always walk from the root of the document, whatever the current position
441 // of the document iterator: the user may have already consumed part of the
442 // document, so the iterator's current depth must not be used here.
443 depth_t depth = 1;
444 auto cur_struct_index = stream->doc.iter._root - stream->parser->implementation->structural_indexes.get();
445
446 // Process the first token to determine if scalar value
447 {
448 switch (stream->buf[stream->batch_start + stream->parser->implementation->structural_indexes[cur_struct_index]]) {
449 case '{': case '[': // Depth=1 already at start of document
450 break;
451 case '}': case ']':
452 depth--;
453 break;
454 default: // Scalar value document
455 // This returns a string spanning from start of value to the beginning of the next document (excluded)
456 {
457 auto next_index = stream->batch_start + stream->parser->implementation->structural_indexes[++cur_struct_index];
458 // normally the length would be next_index - current_index() - 1, except for the last document
459 size_t svlen = next_index - current_index();
460 const char *start = reinterpret_cast<const char*>(stream->buf) + current_index();
461 // When the scalar is followed by a truncated document, the structural
462 // indexes of that document were dropped and next_index is the end of
463 // the input, so we bound the scalar by scanning the token itself.
464 size_t token_len = 0;
465 if (*start == '"') {
466 token_len = 1;
467 while (token_len < svlen) {
468 char c = start[token_len++];
469 if (c == '\\') {
470 token_len++;
471 } else if (c == '"') {
472 break;
473 }
474 }
475 } else {
476 while (token_len < svlen) {
477 char c = start[token_len];
478 if (std::isspace(static_cast<unsigned char>(c)) || c == ',' || c == '{' || c == '[' || c == '\0' || static_cast<uint8_t>(c) == 0x1E) {
479 break;
480 }
481 token_len++;
482 }
483 }
484 if (token_len > 0 && token_len < svlen) {
485 svlen = token_len;
486 }
487 // Trim trailing whitespace, NUL, and RS (0x1E). In RFC 7464
488 // json_sequence mode the scanner classifies RS as a scalar
489 // character, so an RS-prefixed scalar document (number / true /
490 // false / null / string) has no closing structural index and the
491 // slice runs all the way up to the next document's RS. RS cannot
492 // legally appear in a JSON value at the source level (control
493 // characters in strings must be escaped as \u001E), so stripping
494 // it is safe in every stream_format.
495 while(svlen > 1 && (std::isspace(static_cast<unsigned char>(start[svlen-1])) || start[svlen-1] == '\0' || static_cast<uint8_t>(start[svlen-1]) == 0x1E || (stream->format == stream_format::comma_delimited && start[svlen-1] == ','))) {
496 svlen--;
497 }
498 return std::string_view(start, svlen);
499 }
500 }
501 cur_struct_index++;
502 }
503
504 while (cur_struct_index <= static_cast<int64_t>(stream->parser->implementation->n_structural_indexes)) {
505 switch (stream->buf[stream->batch_start + stream->parser->implementation->structural_indexes[cur_struct_index]]) {
506 case '{': case '[':
507 depth++;
508 break;
509 case '}': case ']':
510 depth--;
511 break;
512 }
513 if (depth == 0) { break; }
514 cur_struct_index++;
515 }
516
517 return std::string_view(reinterpret_cast<const char*>(stream->buf) + current_index(), stream->parser->implementation->structural_indexes[cur_struct_index] - current_index() + stream->batch_start + 1);;
518}
519
521 return stream->error;
522}
523
524#ifdef SIMDJSON_THREADS_ENABLED
525
526inline void document_stream::load_from_stage1_thread() noexcept {
527 worker->finish();
528 // Swap to the parser that was loaded up in the thread. Make sure the parser has
529 // enough memory to swap to, as well.
530 std::swap(stage1_thread_parser,*parser);
531 error = stage1_thread_error;
532 if (error) { return; }
533
534 // If there's anything left, start the stage 1 thread!
535 if (next_batch_start() < len) {
536 start_stage1_thread();
537 }
538}
539
540inline void document_stream::start_stage1_thread() noexcept {
541 // we call the thread on a lambda that will update
542 // this->stage1_thread_error
543 // there is only one thread that may write to this value
544 // TODO this is NOT exception-safe.
545 this->stage1_thread_error = UNINITIALIZED; // In case something goes wrong, make sure it's an error
546 size_t _next_batch_start = this->next_batch_start();
547
548 worker->run(this, & this->stage1_thread_parser, _next_batch_start);
549}
550
551#endif // SIMDJSON_THREADS_ENABLED
552
553} // namespace ondemand
554} // namespace SIMDJSON_IMPLEMENTATION
555} // namespace simdjson
556
557namespace simdjson {
558
559simdjson_inline simdjson_result<SIMDJSON_IMPLEMENTATION::ondemand::document_stream>::simdjson_result(
560 error_code error
561) noexcept :
562 implementation_simdjson_result_base<SIMDJSON_IMPLEMENTATION::ondemand::document_stream>(error)
563{
564}
565simdjson_inline simdjson_result<SIMDJSON_IMPLEMENTATION::ondemand::document_stream>::simdjson_result(
566 SIMDJSON_IMPLEMENTATION::ondemand::document_stream &&value
567) noexcept :
568 implementation_simdjson_result_base<SIMDJSON_IMPLEMENTATION::ondemand::document_stream>(
569 std::forward<SIMDJSON_IMPLEMENTATION::ondemand::document_stream>(value)
570 )
571{
572}
573
574}
575
576#endif // SIMDJSON_GENERIC_ONDEMAND_DOCUMENT_STREAM_INL_H
iterator & operator++() noexcept
Advance to the next document (prefix).
simdjson_inline bool operator!=(const iterator &other) const noexcept
Check if we're at the end yet.
error_code error() const noexcept
Returns error of the stream (if any).
simdjson_inline reference operator*() noexcept
Get the current document (or error).
bool at_end() const noexcept
Returns whether the iterator is at the end.
simdjson_inline iterator end() noexcept
The end of the stream, for iterator comparison purposes.
size_t size_in_bytes() const noexcept
Returns the input size in bytes.
size_t truncated_bytes() const noexcept
After iterating through the stream, this method returns the number of bytes that were not parsed at t...
simdjson_inline iterator begin() noexcept
Start iterating the documents in the stream.
simdjson_inline document_stream() noexcept
Construct an uninitialized document_stream.
json_iterator iter
Current position in the document.
Definition document.h:934
int32_t depth_t
Represents the depth of a JSON value (number of nested arrays/objects).
Definition base.h:18
The top level simdjson namespace, containing everything the library provides.
Definition base.h:8
stream_format
Stream format for parse_many/iterate_many.
Definition base.h:61
@ comma_delimited
Comma-separated JSON documents (e.g., {...},{...},{...})
@ whitespace_delimited
Whitespace-delimited JSON documents (default, includes NDJSON/JSONL)
@ newline_delimited
NDJSON/JSON Lines where each document occupies exactly one line: documents are separated by line feed...
@ json_sequence
RFC 7464 JSON text sequences (RS-delimited)
error_code
All possible errors returned by simdjson.
Definition error.h:19
@ CAPACITY
This parser can't support a document that big.
Definition error.h:21
@ EMPTY
no structural element found
Definition error.h:33
@ MEMALLOC
Error allocating memory, most likely out of memory.
Definition error.h:22
@ SUCCESS
No error.
Definition error.h:20
@ UNINITIALIZED
unknown error, or uninitialized document
Definition error.h:32
stage1_mode
This enum is used with the dom_parser_implementation::stage1 function.
The result of a simdjson operation that could fail.
Definition error.h:281