use crate::{
ast::Value,
error::{Error, Result},
parse_with_options,
parser::ParserOptions,
};
use rayon::prelude::*;
use std::sync::Arc;
type ParseResult = (Option<Value>, Option<(usize, Error)>);
type MergedResults = (Vec<Value>, Vec<(usize, Error)>);
#[derive(Debug, Clone)]
pub struct ChunkedConfig {
pub chunk_size: usize,
pub max_threads: usize,
pub boundary_buffer: usize,
pub validate_chunks: bool,
}
impl Default for ChunkedConfig {
fn default() -> Self {
Self {
chunk_size: 1024 * 1024, max_threads: 0, boundary_buffer: 4096, validate_chunks: true,
}
}
}
#[derive(Debug, Clone)]
pub struct JsonChunk {
pub start: usize,
pub end: usize,
pub data: String,
pub is_value_start: bool,
pub is_value_end: bool,
pub start_nesting: i32,
pub end_nesting: i32,
}
#[derive(Debug)]
pub struct ChunkedResult {
pub values: Vec<Value>,
pub errors: Vec<(usize, Error)>,
pub stats: ProcessingStats,
}
#[derive(Debug, Default)]
pub struct ProcessingStats {
pub chunks_processed: usize,
pub bytes_processed: usize,
pub split_time_ms: u64,
pub parse_time_ms: u64,
pub merge_time_ms: u64,
pub peak_memory: usize,
}
pub struct ChunkedProcessor {
config: ChunkedConfig,
}
impl Default for ChunkedProcessor {
fn default() -> Self {
Self::new(ChunkedConfig::default())
}
}
impl ChunkedProcessor {
pub fn new(config: ChunkedConfig) -> Self {
Self { config }
}
pub fn parse(&self, input: &str, options: ParserOptions) -> Result<ChunkedResult> {
let start_time = std::time::Instant::now();
if input.len() < self.config.chunk_size {
let value = parse_with_options(input, options)?;
return Ok(ChunkedResult {
values: vec![value],
errors: vec![],
stats: ProcessingStats {
chunks_processed: 1,
bytes_processed: input.len(),
parse_time_ms: start_time.elapsed().as_millis() as u64,
..Default::default()
},
});
}
let mut stats = ProcessingStats::default();
let split_start = std::time::Instant::now();
let chunks = self.split_into_chunks(input)?;
stats.split_time_ms = split_start.elapsed().as_millis() as u64;
stats.chunks_processed = chunks.len();
stats.bytes_processed = input.len();
let parse_start = std::time::Instant::now();
let options = Arc::new(options);
let results: Vec<_> = chunks
.into_par_iter()
.map(|chunk| {
let options = Arc::clone(&options);
self.parse_chunk(chunk, &options)
})
.collect();
stats.parse_time_ms = parse_start.elapsed().as_millis() as u64;
let merge_start = std::time::Instant::now();
let merged = self.merge_results(results)?;
stats.merge_time_ms = merge_start.elapsed().as_millis() as u64;
Ok(ChunkedResult {
values: merged.0,
errors: merged.1,
stats,
})
}
fn split_into_chunks(&self, input: &str) -> Result<Vec<JsonChunk>> {
let mut chunks = Vec::new();
let bytes = input.as_bytes();
let mut pos = 0;
let mut nesting_level = 0;
let mut in_string = false;
let _escape_next = false;
while pos < bytes.len() {
let chunk_start = pos;
let mut chunk_end = std::cmp::min(pos + self.config.chunk_size, bytes.len());
if chunk_end < bytes.len() {
chunk_end =
self.find_safe_boundary(bytes, chunk_end, &mut nesting_level, &mut in_string)?;
}
let chunk_data = std::str::from_utf8(&bytes[chunk_start..chunk_end])
.map_err(|_| Error::InvalidUtf8(chunk_start))?;
let start_nesting = nesting_level;
let (end_nesting, is_complete) = self.analyze_chunk_nesting(chunk_data)?;
chunks.push(JsonChunk {
start: chunk_start,
end: chunk_end,
data: chunk_data.to_string(),
is_value_start: chunk_start == 0 || start_nesting == 0,
is_value_end: chunk_end == bytes.len() || is_complete,
start_nesting,
end_nesting,
});
pos = chunk_end;
nesting_level = end_nesting;
}
Ok(chunks)
}
fn find_safe_boundary(
&self,
bytes: &[u8],
mut pos: usize,
nesting_level: &mut i32,
in_string: &mut bool,
) -> Result<usize> {
let start_pos = pos;
let max_search = std::cmp::min(pos + self.config.boundary_buffer, bytes.len());
while pos < max_search {
match bytes[pos] {
b'"' if !*in_string => *in_string = true,
b'"' if *in_string => *in_string = false,
b'\\' if *in_string => {
pos += 1; continue;
}
b'{' | b'[' if !*in_string => *nesting_level += 1,
b'}' | b']' if !*in_string => {
*nesting_level -= 1;
if *nesting_level == 0 {
return Ok(pos + 1);
}
}
b',' if !*in_string && *nesting_level == 1 => {
return Ok(pos + 1);
}
b'\n' if !*in_string && *nesting_level == 0 => {
return Ok(pos + 1);
}
_ => {}
}
pos += 1;
}
Ok(start_pos)
}
fn analyze_chunk_nesting(&self, chunk: &str) -> Result<(i32, bool)> {
let mut nesting = 0;
let mut in_string = false;
let mut has_complete_value = false;
for ch in chunk.chars() {
match ch {
'"' => in_string = !in_string,
'{' | '[' if !in_string => nesting += 1,
'}' | ']' if !in_string => {
nesting -= 1;
if nesting == 0 {
has_complete_value = true;
}
}
_ => {}
}
}
Ok((nesting, has_complete_value))
}
fn parse_chunk(
&self,
chunk: JsonChunk,
options: &ParserOptions,
) -> (Option<Value>, Option<(usize, Error)>) {
if self.config.validate_chunks {
if chunk.start_nesting != 0 && !chunk.is_value_start {
return (
None,
Some((
chunk.start,
Error::InvalidChunk("Chunk doesn't start at value boundary".to_string()),
)),
);
}
}
match parse_with_options(&chunk.data, options.clone()) {
Ok(value) => (Some(value), None),
Err(error) => (None, Some((chunk.start, error))),
}
}
fn merge_results(&self, results: Vec<ParseResult>) -> Result<MergedResults> {
let mut values = Vec::new();
let mut errors = Vec::new();
for (value, error) in results {
if let Some(v) = value {
values.push(v);
}
if let Some(e) = error {
errors.push(e);
}
}
Ok((values, errors))
}
}
pub fn parse_parallel_chunked(input: &str, options: ParserOptions) -> Result<ChunkedResult> {
let processor = ChunkedProcessor::default();
processor.parse(input, options)
}
pub fn parse_parallel_chunked_with_config(
input: &str,
options: ParserOptions,
config: ChunkedConfig,
) -> Result<ChunkedResult> {
let processor = ChunkedProcessor::new(config);
processor.parse(input, options)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chunked_small_input() {
let input = r#"{"key": "value"}"#;
let result = parse_parallel_chunked(input, ParserOptions::default()).unwrap();
assert_eq!(result.values.len(), 1);
assert!(result.errors.is_empty());
assert_eq!(result.stats.chunks_processed, 1);
}
#[test]
fn test_chunked_ndjson() {
let input = r#"{"data": [{"a": 1}, {"b": 2}, {"c": 3}]}"#;
let config = ChunkedConfig {
chunk_size: 1000, ..Default::default()
};
let result =
parse_parallel_chunked_with_config(input, ParserOptions::default(), config).unwrap();
assert!(!result.values.is_empty());
assert_eq!(result.stats.chunks_processed, 1);
}
#[test]
fn test_chunk_boundary_detection() {
let processor = ChunkedProcessor::default();
let input = r#"[1,2,3,4,5,6,7,8,9,10]"#;
let chunks = processor.split_into_chunks(input).unwrap();
assert!(!chunks.is_empty());
let total_coverage: usize = chunks.iter().map(|c| c.end - c.start).sum();
assert_eq!(total_coverage, input.len());
}
}