1use crate::parser::{LineStream, parse_declared_sections};
6use crate::{error, types};
7use std::path::Path;
8
9pub struct MemchrLines<'a> {
16 bytes: &'a [u8],
17 pos: usize,
18 peeked: Option<&'a str>,
19 check_utf8: bool,
22 utf8_bad: bool,
23 newlines: Option<Vec<usize>>,
26 index_line: usize,
28 line_no: u32,
31}
32
33impl<'a> MemchrLines<'a> {
34 pub fn new(text: &'a str) -> Self {
35 Self::from_parts(text.as_bytes(), false)
36 }
37
38 pub fn from_bytes(bytes: &'a [u8]) -> Self {
44 Self::from_parts(bytes, true)
45 }
46
47 fn from_parts(bytes: &'a [u8], check_utf8: bool) -> Self {
48 Self {
49 bytes,
50 pos: 0,
51 peeked: None,
52 check_utf8,
53 utf8_bad: false,
54 newlines: None,
55 index_line: 0,
56 line_no: 0,
57 }
58 }
59
60 #[inline]
62 pub fn line_no(&self) -> u32 {
63 self.line_no
64 }
65
66 #[inline]
67 fn read_one(&mut self) -> Option<&'a str> {
68 if self.newlines.is_some() {
69 return self.read_one_indexed();
70 }
71 if self.utf8_bad || self.pos >= self.bytes.len() {
72 return None;
73 }
74 let rest = &self.bytes[self.pos..];
75 let (line_bytes, advance) = match memchr::memchr(b'\n', rest) {
76 Some(i) => (&rest[..i], i + 1),
77 None => (rest, rest.len()),
78 };
79 let trimmed = if line_bytes.last() == Some(&b'\r') {
80 &line_bytes[..line_bytes.len() - 1]
81 } else {
82 line_bytes
83 };
84 if self.check_utf8 {
85 match std::str::from_utf8(trimmed) {
86 Ok(s) => {
87 self.accept_line(advance);
88 Some(s)
89 }
90 Err(_) => {
91 self.line_no = self.line_no.saturating_add(1);
95 self.utf8_bad = true;
96 None
97 }
98 }
99 } else {
100 self.accept_line(advance);
101 Some(unsafe { std::str::from_utf8_unchecked(trimmed) })
103 }
104 }
105
106 #[inline]
107 fn accept_line(&mut self, advance: usize) {
108 self.pos += advance;
109 self.line_no = self.line_no.saturating_add(1);
110 }
111
112 #[inline]
113 pub fn next_line(&mut self) -> Option<&'a str> {
114 if let Some(p) = self.peeked.take() {
115 return Some(p);
116 }
117 self.read_one()
118 }
119
120 #[inline]
121 pub fn peek_line(&mut self) -> Option<&'a str> {
122 if self.peeked.is_none() {
123 self.peeked = self.read_one();
124 }
125 self.peeked
126 }
127
128 fn clear_peek(&mut self) {
130 if let Some(p) = self.peeked.take() {
131 let start = p.as_ptr() as usize - self.bytes.as_ptr() as usize;
133 self.pos = start;
134 self.line_no = self.line_no.saturating_sub(1);
138 if let Some(nl) = self.newlines.as_deref() {
139 self.index_line = line_no_for_pos(nl, self.bytes.len(), start);
140 }
141 }
142 }
143
144 fn read_one_indexed(&mut self) -> Option<&'a str> {
150 if self.utf8_bad || self.pos >= self.bytes.len() {
151 return None;
152 }
153 let len = self.bytes.len();
154 let line_no = {
155 let nl = self.newlines.as_deref().unwrap();
156 if self.pos == line_start_pos(nl, len, self.index_line) {
157 self.index_line
158 } else {
159 line_no_for_pos(nl, len, self.pos)
160 }
161 };
162 let span = indexed_span(self.newlines.as_deref().unwrap(), self.bytes, line_no)?;
163 let trimmed = &self.bytes[span.start..span.content_end];
164 if self.check_utf8 {
165 match std::str::from_utf8(trimmed) {
166 Ok(s) => {
167 self.pos = span.next_pos;
168 self.index_line = line_no + 1;
169 self.line_no = self.index_line as u32;
170 Some(s)
171 }
172 Err(_) => {
173 self.utf8_bad = true;
174 self.index_line = line_no;
175 self.line_no = (line_no as u32).saturating_add(1);
178 None
179 }
180 }
181 } else {
182 self.pos = span.next_pos;
183 self.index_line = line_no + 1;
184 self.line_no = self.index_line as u32;
185 Some(unsafe { std::str::from_utf8_unchecked(trimmed) })
187 }
188 }
189
190 fn utf8_error(&self) -> Option<error::ParseError> {
191 self.utf8_bad.then(invalid_utf8)
192 }
193
194 fn prefer_utf8(&self, err: error::ParseError) -> error::ParseError {
196 if self.utf8_bad { invalid_utf8() } else { err }
197 }
198}
199
200fn invalid_utf8() -> error::ParseError {
201 error::ParseError::ValidationError("invalid UTF-8".into())
202}
203
204impl<'a> Iterator for MemchrLines<'a> {
205 type Item = &'a str;
206 fn next(&mut self) -> Option<&'a str> {
207 self.next_line()
208 }
209}
210
211impl<'a> LineStream<'a> for MemchrLines<'a> {
212 #[inline]
213 fn next_line(&mut self) -> Option<&'a str> {
214 MemchrLines::next_line(self)
215 }
216 #[inline]
217 fn peek_line(&mut self) -> Option<&'a str> {
218 MemchrLines::peek_line(self)
219 }
220 #[inline]
221 fn next_coord_row(
222 &mut self,
223 spec_version: u32,
224 default_id: u64,
225 ) -> Result<([f64; 3], [bool; 3], u64), crate::error::ParseError> {
226 if let Some(p) = self.peeked.take() {
227 return crate::parser::parse_coord_row_at(p.as_bytes(), 0, spec_version, default_id)
228 .map(|(row, _)| row);
229 }
230 self.line_no = self.line_no.saturating_add(1);
233 let (row, pos) =
234 crate::parser::parse_coord_row_at(self.bytes, self.pos, spec_version, default_id)?;
235 self.pos = pos;
236 Ok(row)
237 }
238 #[inline]
239 fn next_vector_row(
240 &mut self,
241 spec_version: u32,
242 ) -> Result<([f64; 3], [bool; 3], u64), crate::error::ParseError> {
243 if let Some(p) = self.peeked.take() {
244 return crate::parser::parse_vector_row_at(p.as_bytes(), 0, spec_version)
245 .map(|(row, _)| row);
246 }
247 let (row, pos) = crate::parser::parse_vector_row_at(self.bytes, self.pos, spec_version)?;
248 self.pos = pos;
249 Ok(row)
250 }
251 fn remainder_is_whitespace(&mut self) -> bool {
252 if self.utf8_bad {
253 return false;
254 }
255 if let Some(line) = self.peek_line()
256 && !line.trim().is_empty()
257 {
258 return false;
259 }
260 if self.utf8_bad {
263 return false;
264 }
265 self.bytes[self.pos..]
266 .iter()
267 .all(|b| b.is_ascii_whitespace())
268 }
269 fn blank_line_opens_section(&mut self) -> bool {
270 let Some(peeked) = self.peeked else {
272 return false;
273 };
274 if !peeked.trim().is_empty() {
275 return false;
276 }
277 match probe_section(&self.bytes[self.pos..]) {
278 SectionProbe::Opens => true,
279 SectionProbe::Closed => false,
280 SectionProbe::InvalidUtf8 => {
283 self.utf8_bad = true;
284 false
285 }
286 }
287 }
288
289 fn blank_opens_velocity_block(&mut self) -> bool {
290 match self.peek_line() {
291 Some(line) if line.trim().is_empty() => self.blank_line_opens_section(),
292 _ => true,
293 }
294 }
295}
296
297fn split_raw_line(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
298 if bytes.is_empty() {
299 return None;
300 }
301 match memchr::memchr(b'\n', bytes) {
302 Some(i) => Some((&bytes[..i], &bytes[i + 1..])),
303 None => Some((bytes, &[])),
304 }
305}
306
307fn strip_cr(line: &[u8]) -> &[u8] {
308 if line.last() == Some(&b'\r') {
309 &line[..line.len() - 1]
310 } else {
311 line
312 }
313}
314
315enum SectionProbe {
316 Opens,
317 Closed,
318 InvalidUtf8,
319}
320
321fn probe_section(after_blank: &[u8]) -> SectionProbe {
324 let Some((raw1, after1)) = split_raw_line(after_blank) else {
325 return SectionProbe::Closed;
326 };
327 let line1 = strip_cr(raw1);
328 if line1.iter().all(|b| matches!(b, b' ' | b'\t')) {
329 return SectionProbe::Closed;
330 }
331 let Ok(text1) = std::str::from_utf8(line1) else {
332 return SectionProbe::InvalidUtf8;
333 };
334 if text1.trim_start().starts_with('{') {
335 return SectionProbe::Closed;
336 }
337 let Some((raw2, _)) = split_raw_line(after1) else {
338 return SectionProbe::Closed;
339 };
340 let Ok(text2) = std::str::from_utf8(strip_cr(raw2)) else {
341 return SectionProbe::InvalidUtf8;
342 };
343 if crate::parser::line_has_component_label(text2) {
344 SectionProbe::Opens
345 } else {
346 SectionProbe::Closed
347 }
348}
349
350fn blank_opens_section(after_blank: &[u8]) -> bool {
351 matches!(probe_section(after_blank), SectionProbe::Opens)
352}
353
354pub struct ConFrameIterator<'a> {
365 pub(crate) lines: MemchrLines<'a>,
366 frame_index: u64,
370}
371
372#[cfg(target_arch = "x86_64")]
380#[target_feature(enable = "avx2,popcnt")]
381#[inline]
382unsafe fn skip_newlines_avx2(hay: &[u8], n: usize) -> (usize, usize) {
383 use std::arch::x86_64::{
384 __m256i, _mm256_cmpeq_epi8, _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_set1_epi8,
385 _popcnt32,
386 };
387 if n == 0 {
388 return (0, 0);
389 }
390 let mut need = n;
391 let mut offset = 0usize;
392 let ptr = hay.as_ptr();
393 let len = hay.len();
394 let nl = _mm256_set1_epi8(b'\n' as i8);
395 while offset + 32 <= len {
396 let chunk = unsafe { _mm256_loadu_si256(ptr.add(offset) as *const __m256i) };
398 let eq = _mm256_cmpeq_epi8(chunk, nl);
399 let mask = _mm256_movemask_epi8(eq) as u32;
400 let count = _popcnt32(mask as i32) as usize;
401 if count >= need {
402 let mut bits = mask;
403 for _ in 1..need {
404 bits &= bits - 1;
405 }
406 let bit = bits.trailing_zeros() as usize;
407 return (n, offset + bit + 1);
408 }
409 need -= count;
410 offset += 32;
411 }
412 while offset < len {
413 if hay[offset] == b'\n' {
414 need -= 1;
415 if need == 0 {
416 return (n, offset + 1);
417 }
418 }
419 offset += 1;
420 }
421 (n - need, len)
422}
423
424impl<'a> ConFrameIterator<'a> {
425 pub fn new(file_contents: &'a str) -> Self {
431 ConFrameIterator {
432 lines: MemchrLines::new(file_contents),
433 frame_index: 0,
434 }
435 }
436
437 pub(crate) fn starting_at(file_contents: &'a str, pos: usize) -> Self {
442 debug_assert!(pos == 0 || file_contents.as_bytes()[pos - 1] == b'\n');
443 let mut it = Self::new(file_contents);
444 it.lines.pos = pos;
445 it
446 }
447
448 pub fn from_bytes(bytes: &'a [u8]) -> Self {
454 ConFrameIterator {
455 lines: MemchrLines::from_bytes(bytes),
456 frame_index: 0,
457 }
458 }
459
460 pub(crate) fn frame_index(&self) -> u64 {
465 self.frame_index
466 }
467
468 fn note_skipped_frame(&mut self) -> Option<Result<(), error::ParseError>> {
469 self.frame_index = self.frame_index.saturating_add(1);
470 Some(Ok(()))
471 }
472
473 #[inline(always)]
483 fn advance_lines(&mut self, n: usize) -> Result<(), error::ParseError> {
484 self.lines.clear_peek();
485 if self.lines.newlines.is_some() {
486 return self.advance_lines_indexed(n);
487 }
488 #[cfg(target_arch = "x86_64")]
489 {
490 if n > 1 && std::is_x86_feature_detected!("avx2") {
491 return self.advance_lines_avx2(n);
492 }
493 }
494 self.advance_lines_memchr(n)
495 }
496
497 fn advance_lines_memchr(&mut self, n: usize) -> Result<(), error::ParseError> {
498 for i in 0..n {
499 let rest = &self.lines.bytes[self.lines.pos..];
500 match memchr::memchr(b'\n', rest) {
501 Some(pos) => {
502 self.lines.pos += pos + 1;
503 self.lines.line_no = self.lines.line_no.saturating_add(1);
504 }
505 None => {
506 if rest.is_empty() {
507 return Err(error::ParseError::IncompleteFrame);
508 }
509 self.lines.pos = self.lines.bytes.len();
510 self.lines.line_no = self.lines.line_no.saturating_add(1);
514 if i + 1 == n {
515 return Ok(());
516 }
517 return Err(error::ParseError::IncompleteFrame);
518 }
519 }
520 }
521 Ok(())
522 }
523
524 #[cfg(target_arch = "x86_64")]
530 #[inline]
531 fn advance_lines_avx2(&mut self, n: usize) -> Result<(), error::ParseError> {
532 let rest = &self.lines.bytes[self.lines.pos..];
533 let (found, end) = unsafe { skip_newlines_avx2(rest, n) };
534 if found == n {
535 self.lines.pos += end;
536 self.lines.line_no = self
539 .lines
540 .line_no
541 .saturating_add(u32::try_from(n).unwrap_or(u32::MAX));
542 return Ok(());
543 }
544 if rest.is_empty() {
545 return Err(error::ParseError::IncompleteFrame);
546 }
547 let has_tail = rest.last().copied() != Some(b'\n');
548 self.lines.pos = self.lines.bytes.len();
549 let counted = found + usize::from(has_tail);
550 self.lines.line_no = self
551 .lines
552 .line_no
553 .saturating_add(u32::try_from(counted).unwrap_or(u32::MAX));
554 if counted >= n {
555 Ok(())
556 } else {
557 Err(error::ParseError::IncompleteFrame)
558 }
559 }
560
561 fn advance_lines_to(&mut self, n: usize, end: Option<usize>) -> Result<(), error::ParseError> {
567 if self.lines.newlines.is_some() {
570 return self.advance_lines(n);
571 }
572 self.lines.clear_peek();
573 let pos = self.lines.pos;
574 let bytes = self.lines.bytes;
575 if let Some(end) = end
576 && pos < end
577 && end <= bytes.len()
578 && bytes[end - 1] == b'\n'
579 && memchr::memchr_iter(b'\n', &bytes[pos..end]).count() == n
580 {
581 self.lines.pos = end;
582 self.lines.line_no = self
585 .lines
586 .line_no
587 .saturating_add(u32::try_from(n).unwrap_or(u32::MAX));
588 return Ok(());
589 }
590 self.advance_lines(n)
591 }
592
593 fn advance_lines_indexed(&mut self, n: usize) -> Result<(), error::ParseError> {
598 let len = self.lines.bytes.len();
599 let step = {
600 let nl = self.lines.newlines.as_deref().unwrap();
601 let nlines = line_count(nl, len);
602 let line_no = if self.lines.pos == line_start_pos(nl, len, self.lines.index_line) {
603 self.lines.index_line
604 } else {
605 line_no_for_pos(nl, len, self.lines.pos)
606 };
607 if n == 0 {
608 IndexStep::Stay(line_no)
609 } else if line_no >= nlines {
610 IndexStep::Missing(line_no)
611 } else {
612 match line_no.checked_add(n) {
613 Some(target) if target <= nlines => {
614 let pos = if target == nlines {
615 len
616 } else {
617 line_start_pos(nl, len, target)
618 };
619 IndexStep::Landed(target, pos)
620 }
621 _ => IndexStep::Short(nlines, len),
622 }
623 }
624 };
625 match step {
626 IndexStep::Stay(line_no) => {
627 self.lines.index_line = line_no;
628 self.lines.line_no = u32::try_from(line_no).unwrap_or(u32::MAX);
629 Ok(())
630 }
631 IndexStep::Missing(line_no) => {
632 self.lines.index_line = line_no;
633 self.lines.line_no = u32::try_from(line_no).unwrap_or(u32::MAX);
634 Err(error::ParseError::IncompleteFrame)
635 }
636 IndexStep::Landed(line_no, pos) => {
637 self.lines.index_line = line_no;
638 self.lines.line_no = u32::try_from(line_no).unwrap_or(u32::MAX);
639 self.lines.pos = pos;
640 Ok(())
641 }
642 IndexStep::Short(line_no, pos) => {
643 self.lines.index_line = line_no;
644 self.lines.line_no = u32::try_from(line_no).unwrap_or(u32::MAX);
645 self.lines.pos = pos;
646 Err(error::ParseError::IncompleteFrame)
647 }
648 }
649 }
650
651 fn read_line_str(&mut self) -> Option<&'a str> {
653 self.lines.clear_peek();
654 self.lines.next_line()
655 }
656
657 pub fn forward_fast(&mut self) -> Option<Result<(), error::ParseError>> {
661 self.forward_fast_to(None)
662 }
663
664 #[inline(always)]
669 pub(crate) fn forward_fast_to(
670 &mut self,
671 end: Option<usize>,
672 ) -> Option<Result<(), error::ParseError>> {
673 self.lines.clear_peek();
674 if self.lines.pos >= self.lines.bytes.len() {
675 return None;
676 }
677 if let Err(e) = self.advance_lines(6) {
679 return Some(Err(e));
680 }
681 let natm_types: usize = match self.read_line_str() {
683 Some(line) => match crate::parser::parse_count_line(line, 1) {
684 Ok(v) => v[0],
685 Err(e) => return Some(Err(e)),
686 },
687 None => return Some(Err(error::ParseError::IncompleteHeader)),
688 };
689 if natm_types == 0 {
690 return match crate::parser::read_zero_atom_tail(|| self.read_line_str()) {
693 Ok(()) => self.note_skipped_frame(),
694 Err(e) => Some(Err(e)),
695 };
696 }
697 let natms_per_type: Vec<usize> = match self.read_line_str() {
699 Some(line) => match crate::parser::parse_count_line(line, natm_types) {
700 Ok(v) => v,
701 Err(e) => return Some(Err(e)),
702 },
703 None => return Some(Err(error::ParseError::IncompleteHeader)),
704 };
705 if natms_per_type.contains(&0) {
706 return Some(Err(error::ParseError::InvalidCount(
707 "each per-type count must be >= 1".into(),
708 )));
709 }
710 if let Err(e) = self.advance_lines(1) {
712 return Some(Err(e));
713 }
714 let total_atoms: usize = natms_per_type.iter().sum();
715 let coord_block_lines = total_atoms + natm_types * 2;
716 if let Err(e) = self.advance_lines_to(coord_block_lines, end) {
717 return Some(Err(e));
718 }
719 self.lines.clear_peek();
721 loop {
722 let rest = &self.lines.bytes[self.lines.pos..];
723 if rest.is_empty() {
724 break;
725 }
726 let next_eol = memchr::memchr(b'\n', rest);
727 let line = match next_eol {
728 Some(pos) => &rest[..pos],
729 None => rest,
730 };
731 let is_blank = line.iter().all(|b| matches!(b, b' ' | b'\t' | b'\r'));
732 if !is_blank {
733 break;
734 }
735 let after = match next_eol {
736 Some(pos) => &rest[pos + 1..],
737 None => &[][..],
738 };
739 if after.iter().all(|b| b.is_ascii_whitespace()) {
742 self.lines.pos = self.lines.bytes.len();
743 break;
744 }
745 if !blank_opens_section(after) {
748 break;
749 }
750 self.lines.pos += next_eol.map(|p| p + 1).unwrap_or(rest.len());
754 self.lines.line_no = self.lines.line_no.saturating_add(1);
757 if let Err(e) = self.advance_lines_to(coord_block_lines, end) {
758 return Some(Err(e));
759 }
760 }
761 self.note_skipped_frame()
762 }
763
764 pub fn forward(&mut self) -> Option<Result<(), error::ParseError>> {
776 self.forward_fast()
778 }
779
780 pub fn byte_pos(&self) -> usize {
782 self.lines.pos
783 }
784
785 pub fn skip_frames(&mut self, n: usize) -> Result<usize, error::ParseError> {
788 self.skip_frames_counted(n).map_err(|(_, err)| err)
789 }
790
791 pub(crate) fn skip_frames_counted(
795 &mut self,
796 n: usize,
797 ) -> Result<usize, (usize, error::ParseError)> {
798 let mut skipped = 0usize;
799 for _ in 0..n {
800 match self.forward() {
801 None => break,
802 Some(Err(e)) => return Err((skipped, e)),
803 Some(Ok(())) => skipped += 1,
804 }
805 }
806 Ok(skipped)
807 }
808
809 pub fn next_with_raw_span(
822 &mut self,
823 file_contents: &'a str,
824 ) -> Option<Result<(types::ConFrame, &'a str), error::ParseError>> {
825 let base = file_contents.as_ptr() as usize;
826 let start = {
827 let line = self.lines.peek_line()?;
828 line.as_ptr() as usize - base
829 };
830 let frame = match self.next()? {
831 Ok(f) => f,
832 Err(e) => return Some(Err(e)),
833 };
834 let end = match self.lines.peek_line() {
835 Some(line) => line.as_ptr() as usize - base,
836 None => file_contents.len(),
837 };
838 debug_assert!(end >= start && end <= file_contents.len());
839 Some(Ok((frame, &file_contents[start..end])))
840 }
841
842 pub(crate) fn next_with_end(
844 &mut self,
845 ) -> Option<Result<(types::ConFrame, usize), error::ParseError>> {
846 let frame = match self.next()? {
847 Ok(f) => f,
848 Err(e) => return Some(Err(e)),
849 };
850 let end = match self.lines.peek_line() {
851 Some(line) => line.as_ptr() as usize - self.lines.bytes.as_ptr() as usize,
852 None => self.lines.pos,
853 };
854 Some(Ok((frame, end)))
855 }
856}
857
858impl<'a> Iterator for ConFrameIterator<'a> {
859 type Item = Result<types::ConFrame, error::ParseError>;
864
865 fn next(&mut self) -> Option<Self::Item> {
872 if let Some(err) = self.lines.utf8_error() {
875 return Some(Err(err));
876 }
877 if self.lines.remainder_is_whitespace() {
881 if let Some(err) = self.lines.utf8_error() {
882 return Some(Err(err));
883 }
884 while self.lines.next_line().is_some() {}
885 return None;
886 }
887 if let Some(err) = self.lines.utf8_error() {
888 return Some(Err(err));
889 }
890 let mut frame = match crate::parser::parse_single_frame_ls(&mut self.lines) {
892 Ok(f) => f,
893 Err(e) => return Some(Err(self.lines.prefer_utf8(e))),
894 };
895 let sections =
899 match parse_declared_sections(&mut self.lines, &mut frame.header, &mut frame.atom_data)
900 {
901 Ok(n) => n,
902 Err(e) => return Some(Err(self.lines.prefer_utf8(e))),
905 };
906 if sections > 0 {
907 frame.install_declared_section_soa();
911 }
912 self.frame_index = self.frame_index.saturating_add(1);
913 Some(Ok(frame))
914 }
915}
916
917#[cfg(test)]
918mod aos_soa_agreement_tests {
919 use super::*;
920 use std::path::PathBuf;
921
922 #[test]
923 fn iterator_vel_forces_soa_matches_aos() {
924 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
925 .join("resources/test/tiny_cuh2_vel_forces.con");
926 let text = std::fs::read_to_string(&p).expect("fixture");
927 let fr = ConFrameIterator::new(&text)
928 .next()
929 .expect("frame")
930 .expect("parse");
931 let n = fr.atom_data.len();
932 assert!(n > 0);
933 assert_eq!(fr.positions.nrows(), n);
934 let has_vel = fr.atom_data.iter().any(|a| a.velocity().is_some());
935 let has_frc = fr.atom_data.iter().any(|a| a.force().is_some());
936 if has_vel {
937 assert_eq!(
938 fr.velocities.nrows(),
939 n,
940 "SoA velocities must match AoS after section parse"
941 );
942 }
943 if has_frc {
944 assert_eq!(fr.forces.nrows(), n, "SoA forces must match AoS");
945 }
946 for (i, a) in fr.atom_data.iter().enumerate() {
947 let p = fr.positions.as_f64_row(i);
948 assert_eq!([a.x, a.y, a.z], p);
949 if let Some(v) = a.velocity() {
950 assert_eq!(v, fr.velocities.as_f64_row(i));
951 }
952 if let Some(f) = a.force() {
953 assert_eq!(f, fr.forces.as_f64_row(i));
954 }
955 }
956 }
957
958 #[test]
961 fn sync_skips_pos_when_nrows_matches_keeps_force_soa() {
962 let p =
963 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2_forces.con");
964 let text = std::fs::read_to_string(&p).expect("fixture");
965 let fr = ConFrameIterator::new(&text)
966 .next()
967 .expect("frame")
968 .expect("parse");
969 let n = fr.atom_data.len();
970 assert_eq!(fr.positions.nrows(), n);
971 assert_eq!(fr.forces.nrows(), n);
972 let p0 = fr.positions.as_f64_row(0);
975 let mut fr2 = fr.clone();
976 fr2.sync_arrays_from_atom_data();
977 assert_eq!(fr2.positions.as_f64_row(0), p0);
978 assert_eq!(fr2.forces.nrows(), n);
979 assert_eq!(
980 fr2.forces.as_f64_row(0),
981 fr2.atom_data[0].force().expect("force")
982 );
983 }
984
985 #[test]
988 fn declared_sections_soa_matches_every_atom() {
989 let n = 400;
990 let mut text = String::new();
991 text.push_str("soa fill\n");
992 text.push_str(
993 "{\"con_spec_version\":2,\"sections\":[\"velocities\",\"forces\",\"energies\"]}\n",
994 );
995 text.push_str("10.0 10.0 10.0\n90.0 90.0 90.0\n0 0\n0 0 0\n1\n");
996 text.push_str(&format!("{n}\n63.546\nCu\nCoordinates of Component 1\n"));
997 for i in 0..n {
998 let x = (i as f64) * 0.125 - 3.5;
999 let y = (i as f64) * -0.05 + 1.0;
1000 let z = (i % 17) as f64 * 0.333;
1001 text.push_str(&format!("{x:.8} {y:.8} {z:.8} 0 {i}\n"));
1002 }
1003 text.push_str("\nCu\nVelocities of Component 1\n");
1004 for i in 0..n {
1005 let vx = (i as f64) * 1.0e-4 - 0.02;
1006 let vy = ((i * 3) % 11) as f64 * -1.0e-3;
1007 let vz = (i as f64).sin() * 0.01;
1008 text.push_str(&format!("{vx:.8} {vy:.8} {vz:.8} 0 {i}\n"));
1009 }
1010 text.push_str("\nCu\nForces of Component 1\n");
1011 for i in 0..n {
1012 let fx = (i as f64) * 0.01;
1013 let fy = -fx / 2.0;
1014 let fz = (i % 5) as f64;
1015 text.push_str(&format!("{fx:.8} {fy:.8} {fz:.8} 0 {i}\n"));
1016 }
1017 text.push_str("\nCu\nEnergies of Component 1\n");
1018 for i in 0..n {
1019 let e = (i as f64) * 0.02 - 4.0;
1020 text.push_str(&format!("{e:.8} 0 {i}\n"));
1021 }
1022 let frame = ConFrameIterator::new(&text)
1023 .next()
1024 .expect("frame")
1025 .expect("parse");
1026 assert_eq!(frame.atom_data.len(), n);
1027 assert_eq!(frame.velocities.nrows(), n);
1028 assert_eq!(frame.forces.nrows(), n);
1029 assert_eq!(frame.atom_energies.len(), n);
1030 assert_eq!(frame.positions.nrows(), n);
1031 assert!(frame.magmoms.nrows() == 0);
1032 for i in 0..n {
1033 let atom = &frame.atom_data[i];
1034 let pos = frame.positions.as_f64_row(i);
1035 assert_eq!(pos[0].to_bits(), atom.x.to_bits());
1036 assert_eq!(pos[1].to_bits(), atom.y.to_bits());
1037 assert_eq!(pos[2].to_bits(), atom.z.to_bits());
1038 let vel = atom.velocity().unwrap();
1039 let soa_v = frame.velocities.as_f64_row(i);
1040 assert_eq!(soa_v[0].to_bits(), vel[0].to_bits());
1041 assert_eq!(soa_v[1].to_bits(), vel[1].to_bits());
1042 assert_eq!(soa_v[2].to_bits(), vel[2].to_bits());
1043 let frc = atom.force().unwrap();
1044 let soa_f = frame.forces.as_f64_row(i);
1045 assert_eq!(soa_f[0].to_bits(), frc[0].to_bits());
1046 assert_eq!(soa_f[1].to_bits(), frc[1].to_bits());
1047 assert_eq!(soa_f[2].to_bits(), frc[2].to_bits());
1048 assert_eq!(
1049 frame.atom_energies.get_f64(i).to_bits(),
1050 atom.energy().unwrap().to_bits()
1051 );
1052 assert_eq!(frame.atom_ids[i], atom.atom_id);
1053 }
1054 }
1055}
1056
1057pub fn read_all_frames(path: &Path) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
1063 read_all_frames_with_threads(path, None)
1064}
1065
1066pub fn frames_from_text(
1082 text: &str,
1083 num_threads: Option<usize>,
1084) -> Result<Vec<types::ConFrame>, error::ParseError> {
1085 #[cfg(feature = "parallel")]
1086 {
1087 if let Some(n) = num_threads.filter(|n| *n >= 2) {
1088 let parts = parse_frames_parallel_with_threads(text, Some(n));
1089 let mut frames = Vec::with_capacity(parts.len());
1090 for r in parts {
1091 frames.push(r?);
1092 }
1093 return Ok(frames);
1094 }
1095 }
1096 #[cfg(not(feature = "parallel"))]
1097 {
1098 let _ = num_threads;
1099 }
1100 ConFrameIterator::new(text).collect()
1101}
1102
1103pub fn read_all_frames_with_threads(
1115 path: &Path,
1116 num_threads: Option<usize>,
1117) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
1118 let bytes = crate::compression::read_bytes_owned(path)?;
1119 Ok(frames_from_bytes_located(bytes.as_slice(), num_threads)?)
1120}
1121
1122#[derive(Debug)]
1124pub struct PositionBlock {
1134 pub n_frames: usize,
1135 pub n_atoms: usize,
1136 pub values: Vec<f64>,
1137}
1138
1139pub fn read_positions_with_threads(
1146 path: &Path,
1147 num_threads: Option<usize>,
1148) -> Result<PositionBlock, Box<dyn std::error::Error>> {
1149 let contents = crate::compression::read_file_contents(path)?;
1150 let block = match &contents {
1151 crate::compression::FileContents::Owned(text) => {
1152 fill_position_block(text.as_bytes(), num_threads)
1153 }
1154 crate::compression::FileContents::Mapped(map) => fill_position_block(map, num_threads),
1155 }?;
1156 Ok(block)
1157}
1158
1159pub struct CoordColumns {
1164 pub block: PositionBlock,
1165 pub masses: Vec<f64>,
1166 pub cells: Vec<f64>,
1167 pub atom_ids: Vec<u64>,
1169 pub fixed: Vec<u8>,
1171 pub species: Vec<Vec<(String, usize)>>,
1176 pub has_section: bool,
1177 pub partial_fixed: bool,
1179}
1180
1181pub fn read_coord_columns(
1186 path: &Path,
1187 num_threads: Option<usize>,
1188) -> Result<CoordColumns, Box<dyn std::error::Error>> {
1189 let contents = crate::compression::read_file_contents(path)?;
1190 let bytes = contents.as_bytes();
1191 if let Ok(Some(columns)) = certified_columns(bytes, num_threads, true) {
1192 return Ok(columns);
1193 }
1194 let frames = frames_from_bytes_located(bytes, num_threads)?;
1195 Ok(columns_from_frames(&frames))
1196}
1197
1198fn certified_columns(
1199 bytes: &[u8],
1200 num_threads: Option<usize>,
1201 identity: bool,
1202) -> Result<Option<CoordColumns>, error::LocatedParseError> {
1203 let mut parsed = match if identity {
1204 crate::columns::positions_with_identity(bytes)
1205 } else {
1206 crate::columns::positions_from_con_bytes(bytes)
1207 } {
1208 Ok(parsed) if parsed.stock => parsed,
1209 _ => return Ok(None),
1210 };
1211 if parsed.n_frames == 0 {
1212 return Ok(Some(CoordColumns {
1213 block: PositionBlock {
1214 n_frames: 0,
1215 n_atoms: 0,
1216 values: Vec::new(),
1217 },
1218 masses: Vec::new(),
1219 cells: Vec::new(),
1220 atom_ids: Vec::new(),
1221 fixed: Vec::new(),
1222 species: Vec::new(),
1223 has_section: false,
1224 partial_fixed: false,
1225 }));
1226 }
1227 let Some(n_atoms) = parsed.rectangular_atoms() else {
1228 let first = parsed.counts[0];
1229 let index = parsed
1230 .counts
1231 .iter()
1232 .position(|count| *count != first)
1233 .unwrap_or(0);
1234 let line = file_line_at_bytes(
1235 bytes,
1236 parsed.spans.get(index).map(|span| span.start).unwrap_or(0),
1237 );
1238 return Err(error::LocatedParseError::new(
1239 error::ParseError::InvalidCount(
1240 "frame atom count does not match the first frame".into(),
1241 ),
1242 line,
1243 index as u64,
1244 ));
1245 };
1246 let ColumnIdentity {
1247 atom_ids,
1248 fixed,
1249 species,
1250 partial_fixed,
1251 } = if identity {
1252 match frame_identity(&parsed, n_atoms) {
1253 Some(identity) => identity,
1254 None => return Ok(None),
1255 }
1256 } else {
1257 ColumnIdentity::default()
1258 };
1259 let has_section = parsed.has_section;
1260 let values = if parallel_column_threads(num_threads) {
1261 parallel_column_values(bytes, &parsed, n_atoms, num_threads)
1262 .map_err(|err| error::LocatedParseError::new(err, 0, 0))?
1263 } else {
1264 std::mem::take(&mut parsed.positions)
1265 };
1266 Ok(Some(CoordColumns {
1267 block: PositionBlock {
1268 n_frames: parsed.n_frames,
1269 n_atoms,
1270 values,
1271 },
1272 masses: parsed.masses,
1273 cells: parsed.cells,
1274 atom_ids,
1275 fixed,
1276 species,
1277 has_section,
1278 partial_fixed,
1279 }))
1280}
1281
1282#[derive(Default)]
1284struct ColumnIdentity {
1285 atom_ids: Vec<u64>,
1286 fixed: Vec<u8>,
1287 species: Vec<Vec<(String, usize)>>,
1288 partial_fixed: bool,
1289}
1290
1291fn frame_identity(parsed: &crate::columns::ConPositions, n_atoms: usize) -> Option<ColumnIdentity> {
1294 let n = parsed.n_frames.saturating_mul(n_atoms);
1295 if parsed.ids.len() != n || parsed.flags.len() != n {
1296 return None;
1297 }
1298 let mut runs = Vec::with_capacity(parsed.n_frames);
1299 let mut components = parsed.components.iter();
1300 for &count in &parsed.counts {
1301 let mut left = count;
1302 let mut frame_runs = Vec::new();
1303 while left > 0 {
1304 let (symbol, width) = components.next()?;
1305 if *width == 0 || *width > left {
1306 return None;
1307 }
1308 frame_runs.push((symbol.to_string(), *width));
1309 left -= *width;
1310 }
1311 runs.push(frame_runs);
1312 }
1313 if components.next().is_some() {
1314 return None;
1315 }
1316 let partial_fixed = parsed.flags.iter().any(|flag| *flag != 0 && *flag != 7);
1317 let species = if runs.windows(2).all(|pair| pair[0] == pair[1]) {
1318 runs.into_iter().take(1).collect()
1319 } else {
1320 runs
1321 };
1322 Some(ColumnIdentity {
1323 atom_ids: parsed.ids.clone(),
1324 fixed: parsed.flags.clone(),
1325 species,
1326 partial_fixed,
1327 })
1328}
1329
1330fn parallel_column_threads(num_threads: Option<usize>) -> bool {
1331 #[cfg(feature = "parallel")]
1334 {
1335 matches!(num_threads, Some(n) if n >= 2)
1336 }
1337 #[cfg(not(feature = "parallel"))]
1338 {
1339 let _ = num_threads;
1340 false
1341 }
1342}
1343
1344fn parallel_column_values(
1345 bytes: &[u8],
1346 parsed: &crate::columns::ConPositions,
1347 n_atoms: usize,
1348 num_threads: Option<usize>,
1349) -> Result<Vec<f64>, error::ParseError> {
1350 let stride = n_atoms * 3;
1351 if stride == 0 {
1352 return Ok(Vec::new());
1353 }
1354 #[cfg(not(feature = "parallel"))]
1355 {
1356 let _ = (bytes, num_threads);
1357 Ok(parsed.positions.clone())
1358 }
1359 #[cfg(feature = "parallel")]
1360 {
1361 let mut values = vec![0.0_f64; parsed.n_frames * stride];
1362 let fill_one = |index: usize, dest: &mut [f64]| {
1363 let span = &parsed.spans[index];
1364 let slice = crate::columns::positions_from_con_bytes(&bytes[span.start..span.end])?;
1365 if slice.counts.first().copied() != Some(n_atoms) || slice.positions.len() != stride {
1366 return Err(error::ParseError::InvalidCount(
1367 "frame atom count does not match the first frame".into(),
1368 ));
1369 }
1370 dest.copy_from_slice(&slice.positions);
1371 Ok(())
1372 };
1373 use rayon::prelude::*;
1374 let mut run = || match values
1375 .par_chunks_mut(stride)
1376 .enumerate()
1377 .find_map_first(|(index, dest)| fill_one(index, dest).err())
1378 {
1379 Some(err) => Err(err),
1380 None => Ok(()),
1381 };
1382 match num_threads {
1383 Some(n) if n >= 2 => {
1384 let pool = rayon::ThreadPoolBuilder::new()
1385 .num_threads(n)
1386 .build()
1387 .map_err(|err| error::ParseError::ValidationError(err.to_string()))?;
1388 pool.install(run)?;
1389 }
1390 _ => run()?,
1391 }
1392 Ok(values)
1393 }
1394}
1395
1396fn columns_from_frames(frames: &[types::ConFrame]) -> CoordColumns {
1397 let n_frames = frames.len();
1398 let n_atoms = frames
1399 .first()
1400 .map(|frame| frame.positions.nrows())
1401 .unwrap_or(0);
1402 let mut values = Vec::with_capacity(n_frames * n_atoms * 3);
1403 let mut masses = Vec::with_capacity(n_frames * n_atoms);
1404 let mut cells = Vec::with_capacity(n_frames * 6);
1405 let mut atom_ids = Vec::with_capacity(n_frames * n_atoms);
1406 let mut fixed = Vec::with_capacity(n_frames * n_atoms);
1407 let mut runs = Vec::with_capacity(n_frames);
1408 let mut has_section = false;
1409 let mut partial_fixed = false;
1410 for frame in frames {
1411 cells.extend_from_slice(&frame.header.boxl);
1412 cells.extend_from_slice(&frame.header.angles);
1413 if let Some(src) = frame.positions.f64_slice() {
1414 values.extend_from_slice(src);
1415 } else {
1416 for row in 0..frame.positions.nrows() {
1417 values.extend_from_slice(&frame.positions.as_f64_row(row));
1418 }
1419 }
1420 for (type_index, &count) in frame.header.natms_per_type.iter().enumerate() {
1421 let mass = frame
1422 .header
1423 .masses_per_type
1424 .get(type_index)
1425 .copied()
1426 .unwrap_or(0.0);
1427 masses.extend(std::iter::repeat_n(mass, count));
1428 }
1429 has_section |= frame.has_velocities()
1430 || frame.has_forces()
1431 || frame.has_energies()
1432 || frame.has_charges()
1433 || frame.has_spins()
1434 || frame.has_magmoms()
1435 || frame.has_displacements()
1436 || frame.has_spreads();
1437 let mut frame_runs: Vec<(String, usize)> = Vec::new();
1438 for atom in &frame.atom_data {
1439 atom_ids.push(atom.atom_id);
1440 let mask = crate::types::encode_fixed_bitmask(atom.fixed);
1441 if mask != 0 && mask != 7 {
1442 partial_fixed = true;
1443 }
1444 fixed.push(mask);
1445 if let Some((symbol, count)) = frame_runs.last_mut()
1446 && symbol == atom.symbol.as_ref()
1447 {
1448 *count += 1;
1449 continue;
1450 }
1451 frame_runs.push((atom.symbol.to_string(), 1));
1452 }
1453 runs.push(frame_runs);
1454 }
1455 let species = if runs.windows(2).all(|pair| pair[0] == pair[1]) {
1456 runs.into_iter().take(1).collect()
1457 } else {
1458 runs
1459 };
1460 CoordColumns {
1461 block: PositionBlock {
1462 n_frames,
1463 n_atoms,
1464 values,
1465 },
1466 masses,
1467 cells,
1468 atom_ids,
1469 fixed,
1470 species,
1471 has_section,
1472 partial_fixed,
1473 }
1474}
1475
1476fn file_line_at_bytes(bytes: &[u8], byte: usize) -> u32 {
1478 let end = byte.min(bytes.len());
1479 1 + memchr::memchr_iter(b'\n', &bytes[..end]).count() as u32
1480}
1481
1482fn share_cursor(local: u32, bytes: &[u8], share_start: usize) -> u32 {
1487 let base = file_line_at_bytes(bytes, share_start);
1488 if local == 0 {
1489 base
1490 } else {
1491 base.saturating_add(local.saturating_sub(1))
1492 }
1493}
1494
1495fn frame_starts(bytes: &[u8]) -> Result<Vec<usize>, error::LocatedParseError> {
1496 let mut scanner = ConFrameIterator::from_bytes(bytes);
1497 let mut boundaries = Vec::new();
1498 loop {
1499 scanner.lines.clear_peek();
1500 let start = scanner.lines.pos;
1501 if start >= bytes.len() || bytes[start..].iter().all(u8::is_ascii_whitespace) {
1502 break;
1503 }
1504 boundaries.push(start);
1505 match scanner.forward_fast() {
1506 Some(Ok(())) => {}
1507 Some(Err(err)) => {
1508 return Err(error::LocatedParseError::new(
1509 err,
1510 scanner.lines.line_no(),
1511 scanner.frame_index(),
1512 ));
1513 }
1514 None => break,
1515 }
1516 }
1517 Ok(boundaries)
1518}
1519
1520fn parse_span_located(
1522 bytes: &[u8],
1523 start: usize,
1524 end: usize,
1525 frame_index: u64,
1526) -> Result<types::ConFrame, error::LocatedParseError> {
1527 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
1528 match iter.next() {
1529 Some(Ok(frame)) => Ok(frame),
1530 Some(Err(err)) => Err(error::LocatedParseError::new(
1531 err,
1532 share_cursor(iter.lines.line_no(), bytes, start),
1533 frame_index,
1534 )),
1535 None => Err(error::LocatedParseError::new(
1536 error::ParseError::IncompleteFrame,
1537 file_line_at_bytes(bytes, start),
1538 frame_index,
1539 )),
1540 }
1541}
1542
1543fn copy_positions(frame: &types::ConFrame, dest: &mut [f64]) -> Result<(), error::ParseError> {
1544 if frame.positions.nrows().saturating_mul(3) != dest.len() {
1545 return Err(error::ParseError::InvalidCount(
1546 "frame atom count does not match the first frame".into(),
1547 ));
1548 }
1549 if let Some(src) = frame.positions.f64_slice() {
1550 dest.copy_from_slice(src);
1551 return Ok(());
1552 }
1553 for (i, slot) in dest.as_chunks_mut::<3>().0.iter_mut().enumerate() {
1554 slot.copy_from_slice(&frame.positions.as_f64_row(i));
1555 }
1556 Ok(())
1557}
1558
1559fn fill_position_block(
1560 bytes: &[u8],
1561 num_threads: Option<usize>,
1562) -> Result<PositionBlock, error::LocatedParseError> {
1563 match certified_columns(bytes, num_threads, false) {
1564 Ok(Some(columns)) => return Ok(columns.block),
1565 Ok(None) => {}
1566 Err(err) => return Err(err),
1567 }
1568 let boundaries = frame_starts(bytes)?;
1569 if boundaries.is_empty() {
1570 return Ok(PositionBlock {
1571 n_frames: 0,
1572 n_atoms: 0,
1573 values: Vec::new(),
1574 });
1575 }
1576 let end0 = boundaries.get(1).copied().unwrap_or(bytes.len());
1577 let first = parse_span_located(bytes, boundaries[0], end0, 0)?;
1578 let n_atoms = first.positions.nrows();
1579 let n_frames = boundaries.len();
1580 let stride = n_atoms.saturating_mul(3);
1581 let mut values = vec![0.0_f64; n_frames.saturating_mul(stride)];
1582 if stride == 0 {
1583 return Ok(PositionBlock {
1584 n_frames,
1585 n_atoms,
1586 values,
1587 });
1588 }
1589 fill_position_spans(bytes, &boundaries, n_atoms, num_threads, &mut values)?;
1590 Ok(PositionBlock {
1591 n_frames,
1592 n_atoms,
1593 values,
1594 })
1595}
1596
1597fn fill_position_spans(
1598 bytes: &[u8],
1599 boundaries: &[usize],
1600 n_atoms: usize,
1601 num_threads: Option<usize>,
1602 values: &mut [f64],
1603) -> Result<(), error::LocatedParseError> {
1604 let stride = n_atoms * 3;
1605 let write_one = |i: usize, dest: &mut [f64]| {
1606 let start = boundaries[i];
1607 let end = boundaries.get(i + 1).copied().unwrap_or(bytes.len());
1608 let frame = parse_span_located(bytes, start, end, i as u64)?;
1609 copy_positions(&frame, dest).map_err(|error| {
1610 error::LocatedParseError::new(error, file_line_at_bytes(bytes, start), i as u64)
1611 })
1612 };
1613 #[cfg(feature = "parallel")]
1614 {
1615 let parallel = !matches!(num_threads, Some(n) if n <= 1);
1616 if parallel {
1617 use rayon::prelude::*;
1618 let mut run = || match values
1623 .par_chunks_mut(stride)
1624 .enumerate()
1625 .find_map_first(|(i, dest)| write_one(i, dest).err())
1626 {
1627 Some(err) => Err(err),
1628 None => Ok(()),
1629 };
1630 return match num_threads {
1631 Some(n) if n >= 2 => {
1632 let pool = rayon::ThreadPoolBuilder::new()
1633 .num_threads(n)
1634 .build()
1635 .map_err(|err| {
1636 error::LocatedParseError::new(
1637 error::ParseError::ValidationError(err.to_string()),
1638 0,
1639 0,
1640 )
1641 })?;
1642 pool.install(run)
1643 }
1644 _ => run(),
1645 };
1646 }
1647 }
1648 #[cfg(not(feature = "parallel"))]
1649 {
1650 let _ = num_threads;
1651 }
1652 for (i, dest) in values.chunks_mut(stride).enumerate() {
1653 write_one(i, dest)?;
1654 }
1655 Ok(())
1656}
1657
1658pub fn frames_from_bytes(
1660 bytes: &[u8],
1661 num_threads: Option<usize>,
1662) -> Result<Vec<types::ConFrame>, error::ParseError> {
1663 #[cfg(feature = "parallel")]
1664 {
1665 if let Some(n) = num_threads.filter(|n| *n >= 2) {
1666 let parts = parse_frames_parallel_bytes(bytes, Some(n));
1667 let mut frames = Vec::with_capacity(parts.len());
1668 for r in parts {
1669 frames.push(r?);
1670 }
1671 return Ok(frames);
1672 }
1673 }
1674 #[cfg(not(feature = "parallel"))]
1675 {
1676 let _ = num_threads;
1677 }
1678 let mut frames = Vec::new();
1679 for r in ConFrameIterator::from_bytes(bytes) {
1680 frames.push(r?);
1681 }
1682 Ok(frames)
1683}
1684
1685fn frames_from_bytes_located(
1687 bytes: &[u8],
1688 num_threads: Option<usize>,
1689) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
1690 #[cfg(feature = "parallel")]
1691 {
1692 if let Some(n) = num_threads.filter(|n| *n >= 2) {
1693 let timed = parse_bytes_timed(bytes, Some(n));
1694 return collect_located_slots(timed.slots, &timed.fail_lines);
1695 }
1696 }
1697 #[cfg(not(feature = "parallel"))]
1698 {
1699 let _ = num_threads;
1700 }
1701 collect_bytes_located(bytes)
1702}
1703
1704fn collect_bytes_located(bytes: &[u8]) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
1706 let mut frames = Vec::new();
1707 let mut iter = ConFrameIterator::from_bytes(bytes);
1708 while let Some(item) = iter.next() {
1709 match item {
1710 Ok(frame) => frames.push(frame),
1711 Err(error) => {
1712 return Err(error::LocatedParseError::new(
1713 error,
1714 iter.lines.line_no(),
1715 iter.frame_index(),
1716 ));
1717 }
1718 }
1719 }
1720 Ok(frames)
1721}
1722
1723#[cfg(feature = "parallel")]
1725fn collect_located_slots(
1726 slots: Vec<Result<types::ConFrame, error::ParseError>>,
1727 fail_lines: &[(usize, u32)],
1728) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
1729 let mut frames = Vec::with_capacity(slots.len());
1730 for (index, part) in slots.into_iter().enumerate() {
1731 match part {
1732 Ok(frame) => frames.push(frame),
1733 Err(error) => {
1734 let line = fail_lines
1735 .iter()
1736 .find(|(slot, _)| *slot == index)
1737 .map(|(_, line)| *line)
1738 .unwrap_or(0);
1739 return Err(error::LocatedParseError::new(error, line, index as u64));
1740 }
1741 }
1742 }
1743 Ok(frames)
1744}
1745
1746#[cfg(feature = "parallel")]
1747fn span_end(boundaries: &[usize], index: usize, len: usize) -> usize {
1748 if index + 1 < boundaries.len() {
1749 boundaries[index + 1]
1750 } else {
1751 len
1752 }
1753}
1754
1755#[cfg(feature = "parallel")]
1760#[derive(Copy, Clone)]
1761struct SlotWritePtr(*mut std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>);
1762
1763#[cfg(feature = "parallel")]
1764impl SlotWritePtr {
1765 unsafe fn write_slot(self, index: usize, value: Result<types::ConFrame, error::ParseError>) {
1768 unsafe {
1769 (*self.0.add(index)).write(value);
1770 }
1771 }
1772}
1773
1774#[cfg(feature = "parallel")]
1778unsafe impl Send for SlotWritePtr {}
1779
1780#[cfg(feature = "parallel")]
1783unsafe impl Sync for SlotWritePtr {}
1784
1785#[cfg(feature = "parallel")]
1790fn thread_minflt() -> i64 {
1791 #[cfg(target_os = "linux")]
1792 {
1793 unsafe {
1794 let mut usage = std::mem::zeroed::<libc::rusage>();
1795 libc::getrusage(libc::RUSAGE_THREAD, &mut usage);
1796 usage.ru_minflt
1797 }
1798 }
1799 #[cfg(not(target_os = "linux"))]
1800 {
1801 0
1802 }
1803}
1804
1805#[cfg(feature = "parallel")]
1811fn sample_llc_misses() -> i64 {
1812 #[cfg(not(target_os = "linux"))]
1814 {
1815 return -1;
1816 }
1817 #[cfg(target_os = "linux")]
1818 {
1819 #[repr(C)]
1820 struct PerfEventAttr {
1821 typ: u32,
1822 size: u32,
1823 config: u64,
1824 rest: [u64; 14],
1825 }
1826 let attr = PerfEventAttr {
1827 typ: 3,
1828 size: std::mem::size_of::<PerfEventAttr>() as u32,
1829 config: 2 | (1 << 16),
1831 rest: [0; 14],
1832 };
1833 let fd = unsafe {
1834 libc::syscall(
1835 libc::SYS_perf_event_open,
1836 &attr as *const PerfEventAttr,
1837 0i32,
1838 -1i32,
1839 -1i32,
1840 0u64,
1841 )
1842 };
1843 if fd < 0 {
1844 return -1;
1845 }
1846 let fd = fd as libc::c_int;
1847 let mut value = 0u64;
1848 let n = unsafe { libc::read(fd, &mut value as *mut u64 as *mut libc::c_void, 8) };
1849 unsafe {
1850 libc::close(fd);
1851 }
1852 if n == 8 { value as i64 } else { -1 }
1853 }
1854}
1855
1856#[cfg(feature = "parallel")]
1862fn numa_remote_pages() -> i64 {
1863 let online = std::fs::read_to_string("/sys/devices/system/node/online").unwrap_or_default();
1864 let online = online.trim();
1865 let nodes = if let Some((start, end)) = online.split_once('-') {
1866 let start: i64 = start.parse().unwrap_or(0);
1867 let end: i64 = end.parse().unwrap_or(0);
1868 end - start + 1
1869 } else {
1870 1
1871 };
1872 if nodes <= 1 {
1873 return 0;
1874 }
1875 let text = std::fs::read_to_string("/proc/self/numa_maps").unwrap_or_default();
1876 let mut remote = 0i64;
1877 for line in text.lines() {
1878 for token in line.split_whitespace() {
1879 if let Some(rest) = token.strip_prefix('N')
1880 && let Some((node, pages)) = rest.split_once('=')
1881 && node != "0"
1882 {
1883 remote += pages.parse::<i64>().unwrap_or(0);
1884 }
1885 }
1886 }
1887 remote
1888}
1889
1890#[cfg(feature = "parallel")]
1897fn byte_balanced_ranges(boundaries: &[usize], len: usize, workers: usize) -> Vec<(usize, usize)> {
1898 let n = boundaries.len();
1899 if n == 0 {
1900 return Vec::new();
1901 }
1902 let workers = workers.max(1).min(n);
1903 let chunks = workers.saturating_mul(4).clamp(workers, n);
1904 let origin = boundaries[0];
1905 let total = len.saturating_sub(origin).max(1) as u128;
1906 let mut ranges = Vec::with_capacity(chunks);
1907 let mut start = 0usize;
1908 for chunk in 0..chunks {
1909 if start >= n {
1910 break;
1911 }
1912 let end = if chunk + 1 == chunks {
1913 n
1914 } else {
1915 let target = origin + ((total * (chunk as u128 + 1)) / chunks as u128) as usize;
1916 let mut idx = if start + 1 < n {
1917 boundaries[start + 1..].partition_point(|&b| b < target) + start + 1
1918 } else {
1919 n
1920 };
1921 let min_end = start + 1;
1922 let reserved = chunks - chunk - 1;
1923 let max_end = n.saturating_sub(reserved);
1924 if max_end >= min_end {
1925 idx = idx.clamp(min_end, max_end);
1926 } else {
1927 idx = min_end.min(n);
1928 }
1929 idx
1930 };
1931 if end > start {
1932 ranges.push((start, end));
1933 start = end;
1934 }
1935 }
1936 if start < n {
1937 match ranges.last_mut() {
1938 Some(last) => last.1 = n,
1939 None => ranges.push((start, n)),
1940 }
1941 }
1942 ranges
1943}
1944
1945#[cfg(feature = "parallel")]
1952fn parse_into_slots<F>(
1953 bytes: &[u8],
1954 len: usize,
1955 boundaries: &[usize],
1956 workers: usize,
1957 parse_span: F,
1958) -> Vec<Result<types::ConFrame, error::ParseError>>
1959where
1960 F: Fn(usize, usize) -> Result<types::ConFrame, error::ParseError> + Sync,
1961{
1962 parse_into_slots_timed(bytes, len, boundaries, workers, parse_span).slots
1963}
1964
1965#[cfg(feature = "parallel")]
1967struct SlotParse {
1968 slots: Vec<Result<types::ConFrame, error::ParseError>>,
1969 alloc_s: f64,
1970 work_s: f64,
1971 sum_s: f64,
1972 max_s: f64,
1973 chunk_s: Vec<f64>,
1975 worker_minflt: Vec<i64>,
1977 share_frames: Vec<u64>,
1979 share_bytes: Vec<u64>,
1981 fail_lines: Vec<(usize, u32)>,
1984}
1985
1986#[cfg(feature = "parallel")]
1994fn parse_into_slots_timed<F>(
1995 bytes: &[u8],
1996 len: usize,
1997 boundaries: &[usize],
1998 workers: usize,
1999 parse_span: F,
2000) -> SlotParse
2001where
2002 F: Fn(usize, usize) -> Result<types::ConFrame, error::ParseError> + Sync,
2003{
2004 let n = boundaries.len();
2005 if n == 0 {
2006 return SlotParse {
2007 slots: Vec::new(),
2008 alloc_s: 0.0,
2009 work_s: 0.0,
2010 sum_s: 0.0,
2011 max_s: 0.0,
2012 chunk_s: Vec::new(),
2013 worker_minflt: Vec::new(),
2014 share_frames: Vec::new(),
2015 share_bytes: Vec::new(),
2016 fail_lines: Vec::new(),
2017 };
2018 }
2019 if workers < 2 {
2020 let t_alloc = std::time::Instant::now();
2021 let mut slots = Vec::with_capacity(n);
2022 let alloc_s = t_alloc.elapsed().as_secs_f64();
2023 let flt0 = thread_minflt();
2024 let t_work = std::time::Instant::now();
2025 for i in 0..n {
2026 slots.push(parse_span(boundaries[i], span_end(boundaries, i, len)));
2027 }
2028 let work_s = t_work.elapsed().as_secs_f64();
2029 let worker_minflt = vec![thread_minflt().saturating_sub(flt0)];
2030 return SlotParse {
2031 slots,
2032 alloc_s,
2033 work_s,
2034 sum_s: work_s,
2035 max_s: work_s,
2036 chunk_s: vec![work_s],
2037 worker_minflt,
2038 share_frames: vec![n as u64],
2039 share_bytes: vec![len as u64],
2040 fail_lines: Vec::new(),
2041 };
2042 }
2043 let t_alloc = std::time::Instant::now();
2044 let mut slots = take_slot_buf(n);
2045 let alloc_s = t_alloc.elapsed().as_secs_f64();
2046 let ranges = byte_balanced_ranges(boundaries, len, workers);
2050 let chunk_ns: Vec<std::sync::atomic::AtomicU64> = (0..ranges.len())
2051 .map(|_| std::sync::atomic::AtomicU64::new(0))
2052 .collect();
2053 let chunk_flt: Vec<std::sync::atomic::AtomicI64> = (0..ranges.len())
2054 .map(|_| std::sync::atomic::AtomicI64::new(0))
2055 .collect();
2056 let slots_ptr = SlotWritePtr(slots.as_mut_ptr());
2057 let fail_lines = std::sync::Mutex::new(Vec::<(usize, u32)>::new());
2058 let t_work = std::time::Instant::now();
2059 {
2060 let parse_range = |range_index: usize, start: usize, end: usize| {
2061 if start >= end {
2062 return;
2063 }
2064 let flt0 = thread_minflt();
2065 let t_chunk = std::time::Instant::now();
2066 let byte_start = boundaries[start];
2067 let byte_end = span_end(boundaries, end - 1, len);
2068 let mut iter = ConFrameIterator::from_bytes(&bytes[byte_start..byte_end]);
2069 for i in start..end {
2070 let frame = match iter.next() {
2071 Some(Ok(frame)) => Ok(frame),
2072 Some(Err(error)) => {
2073 let line = share_cursor(iter.lines.line_no(), bytes, byte_start);
2074 fail_lines.lock().expect("fail lines").push((i, line));
2075 Err(error)
2076 }
2077 None => {
2078 let line = share_cursor(iter.lines.line_no(), bytes, byte_start);
2079 fail_lines.lock().expect("fail lines").push((i, line));
2080 Err(error::ParseError::IncompleteFrame)
2081 }
2082 };
2083 unsafe {
2086 slots_ptr.write_slot(i, frame);
2087 }
2088 }
2089 chunk_ns[range_index].store(
2090 t_chunk.elapsed().as_nanos() as u64,
2091 std::sync::atomic::Ordering::Relaxed,
2092 );
2093 chunk_flt[range_index].store(
2094 thread_minflt().saturating_sub(flt0),
2095 std::sync::atomic::Ordering::Relaxed,
2096 );
2097 };
2098 let run_ranges = || {
2099 use rayon::prelude::*;
2100 ranges
2101 .par_iter()
2102 .enumerate()
2103 .for_each(|(range_index, &(start, end))| {
2104 parse_range(range_index, start, end);
2105 });
2106 };
2107 if rayon::current_thread_index().is_some() {
2108 run_ranges();
2109 } else {
2110 match rayon::ThreadPoolBuilder::new().num_threads(workers).build() {
2111 Ok(pool) => pool.install(run_ranges),
2112 Err(_) => {
2113 for (range_index, &(start, end)) in ranges.iter().enumerate() {
2114 parse_range(range_index, start, end);
2115 }
2116 }
2117 }
2118 }
2119 }
2120 let work_s = t_work.elapsed().as_secs_f64();
2121 let mut worker_sum_s = 0.0_f64;
2122 let mut worker_max_s = 0.0_f64;
2123 let mut chunk_s = Vec::new();
2124 let mut worker_minflt = Vec::new();
2125 let mut share_frames = Vec::new();
2126 let mut share_bytes = Vec::new();
2127 for (range_index, &(start, end)) in ranges.iter().enumerate() {
2128 if start >= end {
2129 continue;
2130 }
2131 let seconds =
2132 chunk_ns[range_index].load(std::sync::atomic::Ordering::Relaxed) as f64 / 1.0e9;
2133 chunk_s.push(seconds);
2134 worker_sum_s += seconds;
2135 worker_max_s = worker_max_s.max(seconds);
2136 worker_minflt.push(chunk_flt[range_index].load(std::sync::atomic::Ordering::Relaxed));
2137 share_frames.push((end - start) as u64);
2138 let nbytes = span_end(boundaries, end - 1, len).saturating_sub(boundaries[start]);
2139 share_bytes.push(nbytes as u64);
2140 }
2141 let slots = unsafe {
2144 let mut raw = std::mem::ManuallyDrop::new(slots);
2145 Vec::from_raw_parts(
2146 raw.as_mut_ptr() as *mut Result<types::ConFrame, error::ParseError>,
2147 n,
2148 raw.capacity(),
2149 )
2150 };
2151 let fail_lines = fail_lines.into_inner().unwrap_or_default();
2152 SlotParse {
2153 slots,
2154 alloc_s,
2155 work_s,
2156 sum_s: worker_sum_s,
2157 max_s: worker_max_s,
2158 chunk_s,
2159 worker_minflt,
2160 share_frames,
2161 share_bytes,
2162 fail_lines,
2163 }
2164}
2165
2166#[cfg(feature = "parallel")]
2167fn parse_byte_span(
2168 bytes: &[u8],
2169 start: usize,
2170 end: usize,
2171) -> Result<types::ConFrame, error::ParseError> {
2172 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
2173 match iter.next() {
2174 Some(result) => result,
2175 None => Err(error::ParseError::IncompleteFrame),
2176 }
2177}
2178
2179#[cfg(feature = "parallel")]
2180fn parse_text_span(
2181 text: &str,
2182 start: usize,
2183 end: usize,
2184) -> Result<types::ConFrame, error::ParseError> {
2185 let mut iter = ConFrameIterator::new(&text[start..end]);
2186 match iter.next() {
2187 Some(result) => result,
2188 None => Err(error::ParseError::IncompleteFrame),
2189 }
2190}
2191
2192#[cfg(feature = "parallel")]
2193struct TimedParse {
2194 slots: Vec<Result<types::ConFrame, error::ParseError>>,
2195 pool: Option<std::sync::Arc<rayon::ThreadPool>>,
2198 pool_s: f64,
2199 pool_join_s: f64,
2201 scan_s: f64,
2202 scan_minflt: i64,
2203 slot_alloc_s: f64,
2204 worker_parse_s: f64,
2205 worker_sum_s: f64,
2206 worker_max_s: f64,
2207 chunk_s: Vec<f64>,
2208 worker_minflt: Vec<i64>,
2209 share_frames: Vec<u64>,
2210 share_bytes: Vec<u64>,
2211 fail_lines: Vec<(usize, u32)>,
2213 llc_misses: i64,
2214 numa_remote: i64,
2215 scan_kind: u8,
2217}
2218
2219#[cfg(feature = "parallel")]
2225fn keep_parse_pages() {
2226 #[cfg(target_os = "linux")]
2230 unsafe {
2231 libc::mallopt(-4, 0);
2232 libc::mallopt(-1, i32::MAX);
2233 }
2234}
2235
2236#[cfg(feature = "parallel")]
2242fn take_slot_buf(
2243 n: usize,
2244) -> Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>> {
2245 let mut buf = slot_cache()
2246 .lock()
2247 .map(|mut slot| std::mem::take(&mut *slot))
2248 .unwrap_or_default();
2249 if buf.capacity() < n {
2250 buf = Vec::with_capacity(n);
2251 }
2252 unsafe { buf.set_len(n) };
2253 buf
2254}
2255
2256#[cfg(feature = "parallel")]
2257fn slot_cache()
2258-> &'static std::sync::Mutex<Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>>>
2259{
2260 static CACHE: std::sync::Mutex<
2261 Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>>,
2262 > = std::sync::Mutex::new(Vec::new());
2263 &CACHE
2264}
2265
2266#[cfg(feature = "parallel")]
2271#[allow(clippy::type_complexity)]
2272fn shared_pool(workers: usize) -> Option<std::sync::Arc<rayon::ThreadPool>> {
2273 use std::sync::{Arc, Mutex, OnceLock};
2274 static CACHE: OnceLock<Mutex<Vec<(usize, Arc<rayon::ThreadPool>)>>> = OnceLock::new();
2275 let cache = CACHE.get_or_init(|| Mutex::new(Vec::new()));
2276 let mut guard = cache.lock().ok()?;
2277 if let Some((_, pool)) = guard.iter().find(|(count, _)| *count == workers) {
2278 return Some(Arc::clone(pool));
2279 }
2280 keep_parse_pages();
2281 let pool = Arc::new(
2282 rayon::ThreadPoolBuilder::new()
2283 .num_threads(workers)
2284 .build()
2285 .ok()?,
2286 );
2287 guard.push((workers, Arc::clone(&pool)));
2288 Some(pool)
2289}
2290
2291#[cfg(feature = "parallel")]
2295pub fn process_minflt() -> i64 {
2296 #[cfg(unix)]
2297 {
2298 unsafe {
2299 let mut usage = std::mem::zeroed::<libc::rusage>();
2300 libc::getrusage(libc::RUSAGE_SELF, &mut usage);
2301 usage.ru_minflt
2302 }
2303 }
2304 #[cfg(not(unix))]
2305 {
2306 0
2307 }
2308}
2309
2310#[cfg(feature = "parallel")]
2312fn host_page_size() -> usize {
2313 #[cfg(unix)]
2314 {
2315 let raw = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
2316 if raw > 0 { raw as usize } else { 4096 }
2317 }
2318 #[cfg(not(unix))]
2319 {
2320 4096
2321 }
2322}
2323
2324#[cfg(feature = "parallel")]
2329fn first_touch_f64(values: &mut [f64], workers: usize) {
2330 use rayon::prelude::*;
2331 if values.is_empty() || workers < 2 {
2332 return;
2333 }
2334 let page = host_page_size().max(std::mem::size_of::<f64>());
2335 let step = (page / std::mem::size_of::<f64>()).max(1);
2336 let pages = values.len().div_ceil(step);
2337 let share = pages.div_ceil(workers);
2338 if rayon::current_thread_index().is_some() {
2339 values.par_chunks_mut(step).for_each(|chunk| {
2342 if let Some(slot) = chunk.first_mut() {
2343 *slot = 0.0;
2344 }
2345 });
2346 return;
2347 }
2348 std::thread::scope(|scope| {
2349 let mut rest = values;
2350 for worker in 0..workers {
2351 if rest.is_empty() {
2352 break;
2353 }
2354 let pages_here = share.min(rest.len().div_ceil(step));
2355 let take = (pages_here * step).min(rest.len());
2356 let (head, tail) = rest.split_at_mut(take);
2357 rest = tail;
2358 let _ = worker;
2359 scope.spawn(move || {
2360 let mut at = 0;
2361 while at < head.len() {
2362 head[at] = 0.0;
2363 at += step;
2364 }
2365 });
2366 }
2367 });
2368}
2369
2370#[cfg(feature = "parallel")]
2376fn finish_results(
2377 pool: Option<&rayon::ThreadPool>,
2378 slots: Vec<Result<types::ConFrame, error::ParseError>>,
2379) -> Result<Vec<types::ConFrame>, error::ParseError> {
2380 if slots.len() < 2 || pool.is_none() {
2381 let mut frames = Vec::with_capacity(slots.len());
2382 for part in slots {
2383 frames.push(part?);
2384 }
2385 return Ok(frames);
2386 }
2387 let pool = pool.expect("pool");
2388 pool.install(|| {
2389 use rayon::prelude::*;
2390 if let Some(index) = slots.par_iter().position_first(|item| item.is_err()) {
2391 return Err(match slots.into_iter().nth(index) {
2392 Some(Err(err)) => err,
2393 _ => error::ParseError::IncompleteFrame,
2394 });
2395 }
2396 Ok(slots
2397 .into_par_iter()
2398 .map(|item| item.expect("ok"))
2399 .collect())
2400 })
2401}
2402
2403#[cfg(feature = "parallel")]
2405pub const SCAN_SERIAL: u8 = 0;
2406#[cfg(feature = "parallel")]
2413pub const SCAN_INDEXED: u8 = 1;
2414#[cfg(feature = "parallel")]
2416pub const SCAN_EQUAL_SPAN: u8 = 2;
2417
2418#[cfg(feature = "parallel")]
2426#[cfg_attr(not(test), allow(dead_code))]
2427fn equal_span_boundaries(bytes: &[u8], workers: usize) -> Option<Vec<usize>> {
2428 if workers < 2 || bytes.len() < 2 {
2429 return None;
2430 }
2431 let mut first = ConFrameIterator::from_bytes(bytes);
2432 match first.forward_fast() {
2433 Some(Ok(())) => {}
2434 _ => return None,
2435 }
2436 let span = first.lines.pos;
2437 if span < 2 {
2438 return None;
2439 }
2440 let n = bytes.len() / span;
2441 if n < 2 {
2442 return None;
2443 }
2444 let tiled = n * span;
2447 if bytes[tiled..].iter().any(|b| !b.is_ascii_whitespace()) {
2448 return None;
2449 }
2450 let byte_share = tiled.div_ceil(workers);
2454 for worker in 0..workers {
2455 let byte_cut = worker.saturating_mul(byte_share).min(tiled);
2456 if byte_cut >= tiled {
2457 continue;
2458 }
2459 let index = if worker == 0 {
2460 0
2461 } else {
2462 byte_cut.div_ceil(span).min(n - 1)
2463 };
2464 let start = index.saturating_mul(span);
2465 if start >= tiled {
2466 continue;
2467 }
2468 let end = (start + span).min(tiled);
2469 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
2470 match iter.forward_fast() {
2471 Some(Ok(())) if iter.lines.pos == end - start => {}
2472 _ => return None,
2473 }
2474 }
2475 Some(arithmetic_starts(span, n))
2476}
2477
2478#[cfg(feature = "parallel")]
2483static START_CACHE: std::sync::Mutex<Vec<usize>> = std::sync::Mutex::new(Vec::new());
2484
2485#[cfg(feature = "parallel")]
2486#[cfg_attr(not(test), allow(dead_code))]
2487fn arithmetic_starts(span: usize, n: usize) -> Vec<usize> {
2488 let mut starts = START_CACHE
2489 .lock()
2490 .map(|mut slot| std::mem::take(&mut *slot))
2491 .unwrap_or_default();
2492 if starts.len() != n {
2493 starts.resize(n, 0);
2494 }
2495 for (index, slot) in starts.iter_mut().enumerate() {
2496 *slot = index * span;
2497 }
2498 starts
2499}
2500
2501#[cfg(feature = "parallel")]
2502fn recycle_starts(starts: Vec<usize>) {
2503 if let Ok(mut slot) = START_CACHE.lock()
2504 && starts.capacity() >= slot.capacity()
2505 {
2506 *slot = starts;
2507 }
2508}
2509
2510#[cfg(feature = "parallel")]
2511fn parse_frames_parallel_bytes(
2512 bytes: &[u8],
2513 num_threads: Option<usize>,
2514) -> Vec<Result<types::ConFrame, error::ParseError>> {
2515 parse_bytes_timed(bytes, num_threads).slots
2516}
2517
2518#[cfg(feature = "parallel")]
2525fn parse_bytes_timed(bytes: &[u8], num_threads: Option<usize>) -> TimedParse {
2526 let parse_workers = match num_threads {
2527 Some(n) if n >= 2 => n,
2528 _ => 1,
2529 };
2530 let run = || {
2531 let fault0 = process_minflt();
2532 let t_scan = std::time::Instant::now();
2533 let (boundaries, scan_kind) =
2537 if let Some(bounds) = equal_span_boundaries(bytes, parse_workers) {
2538 (bounds, SCAN_EQUAL_SPAN)
2539 } else {
2540 (
2541 collect_frame_starts(ConFrameIterator::from_bytes(bytes), true),
2542 SCAN_SERIAL,
2543 )
2544 };
2545 let scan_s = t_scan.elapsed().as_secs_f64();
2546 let scan_minflt = process_minflt().saturating_sub(fault0);
2547 let parsed = parse_into_slots_timed(
2548 bytes,
2549 bytes.len(),
2550 &boundaries,
2551 parse_workers,
2552 |start, end| parse_byte_span(bytes, start, end),
2553 );
2554 recycle_starts(boundaries);
2555 (parsed, scan_s, scan_kind, scan_minflt)
2556 };
2557 let (pool_s, pool, packed) = match num_threads {
2558 None => (0.0, None, run()),
2559 Some(n) if n <= 1 => {
2560 let _ = n;
2561 keep_parse_pages();
2562 let fault0 = process_minflt();
2563 let t_scan = std::time::Instant::now();
2564 let boundaries = collect_frame_starts(ConFrameIterator::from_bytes(bytes), true);
2565 let scan_s = t_scan.elapsed().as_secs_f64();
2566 let scan_minflt = process_minflt().saturating_sub(fault0);
2567 let parsed =
2568 parse_into_slots_timed(bytes, bytes.len(), &boundaries, 1, |start, end| {
2569 parse_byte_span(bytes, start, end)
2570 });
2571 (0.0, None, (parsed, scan_s, SCAN_SERIAL, scan_minflt))
2572 }
2573 Some(n) => {
2574 let t_pool = std::time::Instant::now();
2575 match shared_pool(n) {
2576 Some(pool) => {
2577 let built = t_pool.elapsed().as_secs_f64();
2578 let t_wake = std::time::Instant::now();
2579 let (wake, body) = pool.install(|| {
2580 let wake = t_wake.elapsed().as_secs_f64();
2581 (wake, run())
2582 });
2583 (built + wake, Some(pool), body)
2586 }
2587 None => (t_pool.elapsed().as_secs_f64(), None, run()),
2588 }
2589 }
2590 };
2591 let (parsed, scan_s, scan_kind, scan_minflt) = packed;
2592 TimedParse {
2593 slots: parsed.slots,
2594 pool,
2595 pool_s,
2596 pool_join_s: 0.0,
2597 scan_s,
2598 scan_minflt,
2599 slot_alloc_s: parsed.alloc_s,
2600 worker_parse_s: parsed.work_s,
2601 worker_sum_s: parsed.sum_s,
2602 worker_max_s: parsed.max_s,
2603 chunk_s: parsed.chunk_s,
2604 worker_minflt: parsed.worker_minflt,
2605 share_frames: parsed.share_frames,
2606 share_bytes: parsed.share_bytes,
2607 fail_lines: parsed.fail_lines,
2608 llc_misses: sample_llc_misses(),
2609 numa_remote: numa_remote_pages(),
2610 scan_kind,
2611 }
2612}
2613
2614pub fn count_frames(path: &Path) -> Result<usize, LoadError> {
2621 let contents = load_text(path)?;
2622 let text = contents.as_str().map_err(LoadError::Utf8)?;
2623 let mut n = 0usize;
2624 let mut iter = ConFrameIterator::new(text);
2625 loop {
2626 match iter.forward_fast() {
2627 Some(Ok(())) => n += 1,
2628 Some(Err(e)) => {
2629 return Err(LoadError::Parse(error::LocatedParseError::new(
2630 e,
2631 iter.lines.line_no(),
2632 iter.frame_index(),
2633 )));
2634 }
2635 None => break,
2636 }
2637 }
2638 Ok(n)
2639}
2640
2641pub fn read_first_frame(path: &Path) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
2656 let contents = crate::compression::read_file_contents(path)?;
2657 let text = contents.as_str()?;
2658 let mut iter = ConFrameIterator::new(text);
2659 match iter.next() {
2660 Some(Ok(frame)) => Ok(frame),
2661 Some(Err(e)) => Err(Box::new(e)),
2662 None => Err("No frames found in file".into()),
2663 }
2664}
2665
2666pub fn read_nth_frame(
2670 path: &Path,
2671 index: usize,
2672) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
2673 let contents = crate::compression::read_file_contents(path)?;
2674 let text = contents.as_str()?;
2675 read_nth_frame_from_text(text, index)
2676}
2677
2678pub fn read_nth_frame_from_text(
2680 text: &str,
2681 index: usize,
2682) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
2683 let mut iter = ConFrameIterator::new(text);
2684 let skipped = iter.skip_frames(index)?;
2685 if skipped < index {
2686 return Err(format!("frame {index} out of range (only {skipped} frames)").into());
2687 }
2688 match iter.next() {
2689 Some(Ok(frame)) => Ok(frame),
2690 Some(Err(e)) => Err(Box::new(e)),
2691 None => Err(format!("frame {index} out of range").into()),
2692 }
2693}
2694
2695#[cfg(feature = "parallel")]
2701pub fn read_all_frames_indexed(
2702 path: &Path,
2703 num_threads: Option<usize>,
2704) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
2705 let contents = crate::compression::read_file_contents(path)?;
2706 let text = contents.as_str_sliced()?;
2707 let idx = match contents {
2708 crate::compression::FileContents::Mapped(_) => {
2709 crate::frame_index::load_or_build(path, text, true)?
2710 }
2711 crate::compression::FileContents::Owned(_) => {
2712 let on_disk = std::fs::metadata(path)?.len();
2713 crate::frame_index::load_or_build(path, text, on_disk == text.len() as u64)?
2714 }
2715 };
2716 let offs: Vec<usize> = idx.offsets.iter().map(|&o| o as usize).collect();
2717 parse_frames_at_offsets(text, &offs, num_threads)
2718 .into_iter()
2719 .map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error>))
2720 .collect()
2721}
2722
2723enum IndexStep {
2724 Stay(usize),
2725 Missing(usize),
2726 Landed(usize, usize),
2727 Short(usize, usize),
2728}
2729
2730struct IndexedSpan {
2731 start: usize,
2732 content_end: usize,
2733 next_pos: usize,
2734}
2735
2736fn line_count(nl: &[usize], len: usize) -> usize {
2738 if len == 0 {
2739 0
2740 } else if nl.last().copied() == Some(len - 1) {
2741 nl.len()
2742 } else {
2743 nl.len() + 1
2744 }
2745}
2746
2747fn line_start_pos(nl: &[usize], len: usize, line_no: usize) -> usize {
2748 if line_no == 0 {
2749 return 0;
2750 }
2751 if line_no >= line_count(nl, len) {
2752 return len;
2753 }
2754 nl[line_no - 1] + 1
2755}
2756
2757fn line_no_for_pos(nl: &[usize], len: usize, pos: usize) -> usize {
2759 if pos >= len {
2760 return line_count(nl, len);
2761 }
2762 nl.partition_point(|&p| p < pos)
2763}
2764
2765fn indexed_span(nl: &[usize], bytes: &[u8], line_no: usize) -> Option<IndexedSpan> {
2766 let len = bytes.len();
2767 if line_no >= line_count(nl, len) {
2768 return None;
2769 }
2770 let start = if line_no == 0 { 0 } else { nl[line_no - 1] + 1 };
2771 let (raw_end, next_pos) = if line_no < nl.len() {
2772 (nl[line_no], nl[line_no] + 1)
2773 } else {
2774 (len, len)
2775 };
2776 let mut content_end = raw_end;
2777 if content_end > start && bytes[content_end - 1] == b'\r' {
2778 content_end -= 1;
2779 }
2780 Some(IndexedSpan {
2781 start,
2782 content_end,
2783 next_pos,
2784 })
2785}
2786
2787#[cfg(feature = "parallel")]
2789fn equal_cuts(len: usize, n: usize) -> Vec<usize> {
2790 let n = n.max(1);
2791 let mut cuts = Vec::with_capacity(n + 1);
2792 for i in 0..=n {
2793 let at = ((i as u128) * (len as u128) / (n as u128)) as usize;
2794 let at = at.min(len);
2795 if cuts.last().copied() != Some(at) {
2796 cuts.push(at);
2797 }
2798 }
2799 if cuts.last().copied() != Some(len) {
2800 cuts.push(len);
2801 }
2802 cuts
2803}
2804
2805#[cfg(feature = "parallel")]
2812fn newline_positions_at_cuts(bytes: &[u8], cuts: &[usize]) -> Vec<usize> {
2813 let spans: Vec<(usize, usize)> = cuts
2814 .windows(2)
2815 .map(|w| (w[0].min(bytes.len()), w[1].min(bytes.len())))
2816 .filter(|(start, end)| start < end)
2817 .collect();
2818 if spans.len() <= 1 {
2819 let (start, end) = spans.first().copied().unwrap_or((0, 0));
2820 return memchr::memchr_iter(b'\n', &bytes[start..end])
2821 .map(|p| start + p)
2822 .collect();
2823 }
2824 use rayon::prelude::*;
2828 let parts: Vec<Vec<usize>> = spans
2829 .par_iter()
2830 .map(|&(start, end)| {
2831 memchr::memchr_iter(b'\n', &bytes[start..end])
2832 .map(|p| start + p)
2833 .collect()
2834 })
2835 .collect();
2836 let mut all = Vec::with_capacity(bytes.len() / 32);
2837 for part in parts {
2838 all.extend(part);
2839 }
2840 all
2841}
2842
2843#[cfg(feature = "parallel")]
2844fn newline_positions(bytes: &[u8], threads: usize) -> Vec<usize> {
2845 newline_positions_at_cuts(bytes, &equal_cuts(bytes.len(), threads))
2846}
2847
2848#[cfg(feature = "parallel")]
2858fn scan_thread_count(num_threads: Option<usize>, len: usize) -> usize {
2859 let requested = match num_threads {
2860 Some(n) if n >= 2 => n,
2861 _ => return 1,
2862 };
2863 if len < 2 { 1 } else { requested }
2864}
2865
2866#[derive(Debug)]
2868pub enum LoadError {
2869 Io(std::io::Error),
2870 Utf8(std::str::Utf8Error),
2871 Parse(error::LocatedParseError),
2872 Empty,
2874}
2875
2876impl std::fmt::Display for LoadError {
2877 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2878 match self {
2879 LoadError::Io(e) => write!(f, "{e}"),
2880 LoadError::Utf8(e) => write!(f, "{e}"),
2881 LoadError::Parse(e) => write!(f, "{e}"),
2882 LoadError::Empty => write!(f, "no frames found in file"),
2883 }
2884 }
2885}
2886
2887impl std::error::Error for LoadError {}
2888
2889fn load_error_from_box(err: Box<dyn std::error::Error>) -> LoadError {
2890 if let Some(io) = err.downcast_ref::<std::io::Error>() {
2891 return LoadError::Io(std::io::Error::new(io.kind(), io.to_string()));
2892 }
2893 LoadError::Io(std::io::Error::other(err.to_string()))
2894}
2895
2896fn load_text(path: &Path) -> Result<crate::compression::FileContents, LoadError> {
2897 crate::compression::read_file_contents(path).map_err(load_error_from_box)
2898}
2899
2900#[cfg(feature = "parallel")]
2902fn file_line_at(text: &str, byte: usize) -> u32 {
2903 let end = byte.min(text.len());
2904 1 + text.as_bytes()[..end]
2905 .iter()
2906 .filter(|&&b| b == b'\n')
2907 .count() as u32
2908}
2909
2910pub fn frames_from_text_located(
2912 text: &str,
2913 num_threads: Option<usize>,
2914) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
2915 #[cfg(feature = "parallel")]
2916 {
2917 let use_parallel = matches!(num_threads, Some(n) if n >= 2);
2920 if use_parallel {
2921 let pool = num_threads.filter(|&n| n >= 2);
2922 return collect_parallel_located(text, pool);
2923 }
2924 }
2925 #[cfg(not(feature = "parallel"))]
2926 {
2927 let _ = num_threads;
2928 }
2929 let mut frames = Vec::new();
2930 let mut iter = ConFrameIterator::new(text);
2931 let mut index = 0u64;
2932 while let Some(item) = iter.next() {
2933 match item {
2934 Ok(frame) => {
2935 frames.push(frame);
2936 index += 1;
2937 }
2938 Err(e) => {
2939 return Err(error::LocatedParseError::new(
2940 e,
2941 iter.lines.line_no(),
2942 index,
2943 ));
2944 }
2945 }
2946 }
2947 Ok(frames)
2948}
2949
2950pub fn read_all_frames_located(
2952 path: &Path,
2953 num_threads: Option<usize>,
2954) -> Result<Vec<types::ConFrame>, LoadError> {
2955 let contents = load_text(path)?;
2956 let text = contents.as_str().map_err(LoadError::Utf8)?;
2957 frames_from_text_located(text, num_threads).map_err(LoadError::Parse)
2958}
2959
2960pub fn read_first_frame_located(path: &Path) -> Result<types::ConFrame, LoadError> {
2962 let contents = load_text(path)?;
2963 let text = contents.as_str().map_err(LoadError::Utf8)?;
2964 let mut iter = ConFrameIterator::new(text);
2965 match iter.next() {
2966 Some(Ok(frame)) => Ok(frame),
2967 Some(Err(e)) => Err(LoadError::Parse(error::LocatedParseError::new(
2968 e,
2969 iter.lines.line_no(),
2970 0,
2971 ))),
2972 None => Err(LoadError::Empty),
2973 }
2974}
2975
2976pub fn read_nth_frame_located(path: &Path, index: usize) -> Result<types::ConFrame, LoadError> {
2978 let contents = load_text(path)?;
2979 let text = contents.as_str().map_err(LoadError::Utf8)?;
2980 let mut iter = ConFrameIterator::new(text);
2981 match iter.skip_frames_counted(index) {
2982 Ok(skipped) if skipped == index => {}
2983 Ok(skipped) => {
2984 return Err(LoadError::Parse(error::LocatedParseError::new(
2985 error::ParseError::IncompleteFrame,
2986 iter.lines.line_no(),
2987 skipped as u64,
2988 )));
2989 }
2990 Err((failed_at, e)) => {
2991 return Err(LoadError::Parse(error::LocatedParseError::new(
2992 e,
2993 iter.lines.line_no(),
2994 failed_at as u64,
2995 )));
2996 }
2997 }
2998 match iter.next() {
2999 Some(Ok(frame)) => Ok(frame),
3000 Some(Err(e)) => Err(LoadError::Parse(error::LocatedParseError::new(
3001 e,
3002 iter.lines.line_no(),
3003 index as u64,
3004 ))),
3005 None => Err(LoadError::Parse(error::LocatedParseError::new(
3006 error::ParseError::IncompleteFrame,
3007 iter.lines.line_no(),
3008 index as u64,
3009 ))),
3010 }
3011}
3012
3013fn collect_frame_starts(scanner: ConFrameIterator<'_>, skip_whitespace_tail: bool) -> Vec<usize> {
3014 #[cfg(target_arch = "x86_64")]
3015 {
3016 if std::is_x86_feature_detected!("avx2") {
3017 return unsafe { collect_frame_starts_avx2(scanner, skip_whitespace_tail) };
3021 }
3022 }
3023 collect_frame_starts_inner(scanner, skip_whitespace_tail)
3024}
3025
3026#[cfg(target_arch = "x86_64")]
3027#[target_feature(enable = "avx2,popcnt")]
3028unsafe fn collect_frame_starts_avx2(
3029 scanner: ConFrameIterator<'_>,
3030 skip_whitespace_tail: bool,
3031) -> Vec<usize> {
3032 collect_frame_starts_inner(scanner, skip_whitespace_tail)
3033}
3034
3035#[inline(always)]
3036fn collect_frame_starts_inner(
3037 mut scanner: ConFrameIterator<'_>,
3038 skip_whitespace_tail: bool,
3039) -> Vec<usize> {
3040 let mut boundaries = Vec::new();
3041 loop {
3042 scanner.lines.clear_peek();
3043 let start = scanner.lines.pos;
3044 if start >= scanner.lines.bytes.len() {
3045 break;
3046 }
3047 if skip_whitespace_tail {
3048 let rest = &scanner.lines.bytes[start..];
3049 if rest.iter().all(u8::is_ascii_whitespace) {
3050 break;
3051 }
3052 }
3053 boundaries.push(start);
3054 match scanner.forward_fast_to(None) {
3055 Some(Ok(())) => {}
3056 Some(Err(_)) | None => break,
3057 }
3058 }
3059 boundaries
3060}
3061
3062pub fn frame_start_offsets_serial(file_contents: &str) -> Vec<usize> {
3064 collect_frame_starts(ConFrameIterator::new(file_contents), false)
3065}
3066
3067#[cfg(feature = "parallel")]
3068fn scanner_with_newlines<'a>(text: &'a str, newlines: Vec<usize>) -> ConFrameIterator<'a> {
3069 let mut scanner = ConFrameIterator::new(text);
3070 scanner.lines.index_line = 0;
3071 scanner.lines.newlines = Some(newlines);
3072 scanner
3073}
3074
3075#[cfg(feature = "parallel")]
3076fn frame_start_offsets_indexed(text: &str, newlines: Vec<usize>) -> Vec<usize> {
3077 collect_frame_starts(scanner_with_newlines(text, newlines), false)
3078}
3079
3080#[cfg(all(feature = "parallel", test))]
3086fn frame_start_offsets_at_cuts(text: &str, cuts: &[usize]) -> Vec<usize> {
3087 let newlines = newline_positions_at_cuts(text.as_bytes(), cuts);
3088 frame_start_offsets_indexed(text, newlines)
3089}
3090
3091pub fn frame_start_offsets(file_contents: &str) -> Vec<usize> {
3103 frame_start_offsets_with_threads(file_contents, None)
3104}
3105
3106pub fn frame_start_offsets_with_threads(
3114 file_contents: &str,
3115 num_threads: Option<usize>,
3116) -> Vec<usize> {
3117 #[cfg(feature = "parallel")]
3118 {
3119 let threads = scan_thread_count(num_threads, file_contents.len());
3120 if threads >= 2 {
3121 let newlines = newline_positions(file_contents.as_bytes(), threads);
3122 return frame_start_offsets_indexed(file_contents, newlines);
3123 }
3124 }
3125 #[cfg(not(feature = "parallel"))]
3126 {
3127 let _ = num_threads;
3128 }
3129 frame_start_offsets_serial(file_contents)
3130}
3131
3132#[cfg(feature = "parallel")]
3141pub fn parse_frames_parallel(
3142 file_contents: &str,
3143) -> Vec<Result<types::ConFrame, error::ParseError>> {
3144 parse_frames_parallel_with_threads(file_contents, None)
3145}
3146
3147#[cfg(feature = "parallel")]
3157pub fn parse_frames_parallel_with_threads(
3158 file_contents: &str,
3159 num_threads: Option<usize>,
3160) -> Vec<Result<types::ConFrame, error::ParseError>> {
3161 let parse_workers = match num_threads {
3162 Some(n) if n >= 2 => n,
3163 _ => 1,
3164 };
3165 let run = || {
3166 let boundaries = collect_frame_starts(ConFrameIterator::new(file_contents), true);
3170 parse_into_slots(
3171 file_contents.as_bytes(),
3172 file_contents.len(),
3173 &boundaries,
3174 parse_workers,
3175 |start, end| parse_text_span(file_contents, start, end),
3176 )
3177 };
3178 match num_threads {
3179 Some(n) if n >= 2 => match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
3180 Ok(pool) => pool.install(run),
3181 Err(_) => run(),
3182 },
3183 _ => run(),
3184 }
3185}
3186
3187#[cfg(feature = "parallel")]
3202pub fn parse_frames_at_offsets(
3203 file_contents: &str,
3204 boundaries: &[usize],
3205 num_threads: Option<usize>,
3206) -> Vec<Result<types::ConFrame, error::ParseError>> {
3207 let workers = match num_threads {
3208 Some(n) if n <= 1 => 1,
3209 Some(n) => n,
3210 None => rayon::current_num_threads().max(1),
3211 };
3212 let run = || {
3213 parse_into_slots(
3214 file_contents.as_bytes(),
3215 file_contents.len(),
3216 boundaries,
3217 workers,
3218 |start, end| parse_text_span(file_contents, start, end),
3219 )
3220 };
3221 match num_threads {
3222 Some(n) if n >= 2 => match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
3223 Ok(pool) => pool.install(run),
3224 Err(_) => run(),
3225 },
3226 _ => run(),
3227 }
3228}
3229
3230#[derive(Clone, Copy, Debug, Default)]
3233pub struct ReadPhaseTimes {
3234 pub open_mmap_s: f64,
3235 pub pool_s: f64,
3237 pub pool_join_s: f64,
3240 pub scan_s: f64,
3241 pub slot_alloc_s: f64,
3242 pub worker_parse_s: f64,
3243 pub finish_s: f64,
3245 pub worker_sum_s: f64,
3247 pub worker_max_s: f64,
3249 pub close_s: f64,
3251 pub schedule_s: f64,
3253 pub scan_minflt: i64,
3255 pub parse_minflt: i64,
3257 pub llc_misses: i64,
3259 pub numa_remote: i64,
3261 pub slab_minflt: i64,
3263 pub touch_minflt: i64,
3265 pub mapped: bool,
3267}
3268
3269pub struct PhasedFrames {
3271 pub frames: Vec<types::ConFrame>,
3272 pub phases: ReadPhaseTimes,
3273 pub n_bytes: usize,
3274 pub chunk_s: Vec<f64>,
3276 pub scan_kind: u8,
3279 pub worker_minflt: Vec<i64>,
3281 pub share_frames: Vec<u64>,
3283 pub share_bytes: Vec<u64>,
3285 #[cfg(feature = "parallel")]
3287 pub pool: Option<std::sync::Arc<rayon::ThreadPool>>,
3288}
3289
3290pub fn read_all_frames_phased(
3300 path: &Path,
3301 num_threads: Option<usize>,
3302) -> Result<PhasedFrames, Box<dyn std::error::Error>> {
3303 let t_all = std::time::Instant::now();
3304 let t_open = std::time::Instant::now();
3305 let contents = crate::compression::read_file_contents(path)?;
3306 let open_mmap_s = t_open.elapsed().as_secs_f64();
3307 let mapped = matches!(contents, crate::compression::FileContents::Mapped(_));
3308 let n_bytes = match &contents {
3309 crate::compression::FileContents::Owned(text) => text.len(),
3310 crate::compression::FileContents::Mapped(map) => map.len(),
3311 };
3312 let bytes: &[u8] = match &contents {
3313 crate::compression::FileContents::Owned(text) => text.as_bytes(),
3314 crate::compression::FileContents::Mapped(map) => map,
3315 };
3316 let phased = frames_from_bytes_phased_located(bytes, num_threads)?;
3317 let workers = match num_threads {
3318 Some(n) if n >= 2 => n,
3319 _ => 1,
3320 };
3321 let t_close = std::time::Instant::now();
3322 if let crate::compression::FileContents::Mapped(map) = &contents {
3323 if workers >= 2 {
3328 #[cfg(feature = "parallel")]
3329 {
3330 if let Some(pool) = phased.pool.as_ref() {
3331 pool.install(|| {
3332 crate::compression::advise_dontneed_parallel(map, workers);
3333 });
3334 }
3335 }
3336 #[cfg(not(feature = "parallel"))]
3337 {
3338 let _ = (map, workers);
3339 }
3340 }
3341 }
3342 drop(contents);
3343 let close_s = t_close.elapsed().as_secs_f64();
3344 let elapsed = t_all.elapsed().as_secs_f64();
3345 let named = open_mmap_s
3346 + phased.pool_s
3347 + phased.pool_join_s
3348 + phased.scan_s
3349 + phased.slot_alloc_s
3350 + phased.worker_parse_s
3351 + phased.finish_s
3352 + close_s;
3353 let schedule_s = (elapsed - named).max(0.0);
3354 let parse_minflt = phased.worker_minflt.iter().sum();
3355 Ok(PhasedFrames {
3356 frames: phased.frames,
3357 n_bytes,
3358 chunk_s: phased.chunk_s,
3359 scan_kind: phased.scan_kind,
3360 worker_minflt: phased.worker_minflt,
3361 share_frames: phased.share_frames,
3362 share_bytes: phased.share_bytes,
3363 phases: ReadPhaseTimes {
3364 open_mmap_s,
3365 pool_s: phased.pool_s,
3366 pool_join_s: phased.pool_join_s,
3367 scan_s: phased.scan_s,
3368 slot_alloc_s: phased.slot_alloc_s,
3369 worker_parse_s: phased.worker_parse_s,
3370 finish_s: phased.finish_s,
3371 worker_sum_s: phased.worker_sum_s,
3372 worker_max_s: phased.worker_max_s,
3373 close_s,
3374 schedule_s,
3375 scan_minflt: phased.scan_minflt,
3376 parse_minflt,
3377 llc_misses: phased.llc_misses,
3378 numa_remote: phased.numa_remote,
3379 mapped,
3380 ..ReadPhaseTimes::default()
3381 },
3382 #[cfg(feature = "parallel")]
3383 pool: phased.pool,
3384 })
3385}
3386
3387pub struct PhasedByteTimes {
3393 pub frames: Vec<types::ConFrame>,
3394 pub pool_s: f64,
3395 pub pool_join_s: f64,
3396 pub scan_s: f64,
3397 pub slot_alloc_s: f64,
3398 pub worker_parse_s: f64,
3399 pub finish_s: f64,
3400 pub worker_sum_s: f64,
3401 pub worker_max_s: f64,
3402 pub chunk_s: Vec<f64>,
3404 pub scan_kind: u8,
3407 pub scan_minflt: i64,
3409 pub worker_minflt: Vec<i64>,
3411 pub share_frames: Vec<u64>,
3412 pub share_bytes: Vec<u64>,
3413 pub llc_misses: i64,
3415 pub numa_remote: i64,
3417 #[cfg(feature = "parallel")]
3419 pub pool: Option<std::sync::Arc<rayon::ThreadPool>>,
3420}
3421
3422pub fn frames_from_bytes_phased(
3423 bytes: &[u8],
3424 num_threads: Option<usize>,
3425) -> Result<PhasedByteTimes, error::ParseError> {
3426 frames_from_bytes_phased_located(bytes, num_threads).map_err(|err| err.error)
3427}
3428
3429fn frames_from_bytes_phased_located(
3430 bytes: &[u8],
3431 num_threads: Option<usize>,
3432) -> Result<PhasedByteTimes, error::LocatedParseError> {
3433 #[cfg(feature = "parallel")]
3434 {
3435 if !matches!(num_threads, Some(n) if n >= 2) {
3438 let flt0 = thread_minflt();
3439 let t_work = std::time::Instant::now();
3440 let frames = collect_bytes_located(bytes)?;
3441 let worker_parse_s = t_work.elapsed().as_secs_f64();
3442 let worker_flt = thread_minflt().saturating_sub(flt0);
3443 let n_frames = frames.len() as u64;
3444 return Ok(PhasedByteTimes {
3445 frames,
3446 pool_s: 0.0,
3447 pool_join_s: 0.0,
3448 scan_s: 0.0,
3449 slot_alloc_s: 0.0,
3450 worker_parse_s,
3451 finish_s: 0.0,
3452 worker_sum_s: worker_parse_s,
3453 worker_max_s: worker_parse_s,
3454 chunk_s: vec![worker_parse_s],
3455 scan_kind: SCAN_SERIAL,
3456 scan_minflt: 0,
3457 worker_minflt: vec![worker_flt],
3458 share_frames: vec![n_frames],
3459 share_bytes: vec![bytes.len() as u64],
3460 llc_misses: sample_llc_misses(),
3461 numa_remote: numa_remote_pages(),
3462 pool: None,
3463 });
3464 }
3465 let requested = Some(num_threads.expect("workers"));
3466 let timed = parse_bytes_timed(bytes, requested);
3467 let t_finish = std::time::Instant::now();
3468 let frames = if timed.slots.iter().any(|item| item.is_err()) {
3469 collect_located_slots(timed.slots, &timed.fail_lines)?
3470 } else {
3471 finish_results(timed.pool.as_deref(), timed.slots)
3472 .map_err(|error| error::LocatedParseError::new(error, 0, 0))?
3473 };
3474 let finish_s = t_finish.elapsed().as_secs_f64();
3475 Ok(PhasedByteTimes {
3476 frames,
3477 pool_s: timed.pool_s,
3478 pool_join_s: timed.pool_join_s,
3479 scan_s: timed.scan_s,
3480 slot_alloc_s: timed.slot_alloc_s,
3481 worker_parse_s: timed.worker_parse_s,
3482 finish_s,
3483 worker_sum_s: timed.worker_sum_s,
3484 worker_max_s: timed.worker_max_s,
3485 chunk_s: timed.chunk_s,
3486 scan_kind: timed.scan_kind,
3487 scan_minflt: timed.scan_minflt,
3488 worker_minflt: timed.worker_minflt,
3489 share_frames: timed.share_frames,
3490 share_bytes: timed.share_bytes,
3491 llc_misses: timed.llc_misses,
3492 numa_remote: timed.numa_remote,
3493 pool: timed.pool,
3494 })
3495 }
3496 #[cfg(not(feature = "parallel"))]
3497 {
3498 let _ = num_threads;
3499 let t_work = std::time::Instant::now();
3500 let frames = collect_bytes_located(bytes)?;
3501 let worker_parse_s = t_work.elapsed().as_secs_f64();
3502 Ok(PhasedByteTimes {
3503 frames,
3504 pool_s: 0.0,
3505 pool_join_s: 0.0,
3506 scan_s: 0.0,
3507 slot_alloc_s: 0.0,
3508 worker_parse_s,
3509 finish_s: 0.0,
3510 worker_sum_s: worker_parse_s,
3511 worker_max_s: worker_parse_s,
3512 chunk_s: vec![worker_parse_s],
3513 scan_kind: 0,
3514 scan_minflt: 0,
3515 worker_minflt: Vec::new(),
3516 share_frames: Vec::new(),
3517 share_bytes: Vec::new(),
3518 llc_misses: -1,
3519 numa_remote: 0,
3520 })
3521 }
3522}
3523
3524pub fn pack_position_slab(frames: &[types::ConFrame], workers: usize) -> Option<Vec<f64>> {
3530 if frames.is_empty() {
3531 return None;
3532 }
3533 let n_atoms = frames[0].positions.nrows();
3534 if frames
3535 .iter()
3536 .any(|frame| frame.positions.nrows() != n_atoms)
3537 {
3538 return None;
3539 }
3540 let stride = n_atoms.saturating_mul(3);
3541 let total = frames.len().saturating_mul(stride);
3542 let mut values = Vec::<f64>::with_capacity(total);
3543 if total > 0 {
3544 unsafe {
3548 values.set_len(total);
3549 }
3550 }
3551 if stride == 0 {
3552 return Some(values);
3553 }
3554 let write = |frame: &types::ConFrame, dest: &mut [f64]| {
3555 if let Some(src) = frame.positions.f64_slice() {
3556 dest.copy_from_slice(src);
3557 } else {
3558 for (row, slot) in dest.as_chunks_mut::<3>().0.iter_mut().enumerate() {
3559 slot.copy_from_slice(&frame.positions.as_f64_row(row));
3560 }
3561 }
3562 };
3563 #[cfg(feature = "parallel")]
3564 {
3565 if workers >= 2 {
3566 let chunk_frames = frames.len().div_ceil(workers).max(1);
3567 let chunk_vals = chunk_frames.saturating_mul(stride);
3568 let copy_chunks = |dest_all: &mut [f64]| {
3569 use rayon::prelude::*;
3570 dest_all
3571 .par_chunks_mut(chunk_vals)
3572 .enumerate()
3573 .for_each(|(chunk_index, dest)| {
3574 let base = chunk_index * chunk_frames;
3575 for (offset, slot) in dest.chunks_exact_mut(stride).enumerate() {
3576 let index = base + offset;
3577 if index < frames.len() {
3578 write(&frames[index], slot);
3579 }
3580 }
3581 });
3582 };
3583 if rayon::current_thread_index().is_some() {
3587 first_touch_f64(&mut values, workers);
3588 copy_chunks(&mut values);
3589 } else {
3590 first_touch_f64(&mut values, workers);
3591 std::thread::scope(|scope| {
3592 let mut rest = &mut values[..];
3593 let mut base = 0usize;
3594 let mut handles = Vec::new();
3595 while base < frames.len() {
3596 let take_frames = chunk_frames.min(frames.len() - base);
3597 let take_vals = take_frames * stride;
3598 let (dest, tail) = rest.split_at_mut(take_vals);
3599 rest = tail;
3600 let frame_slice = &frames[base..base + take_frames];
3601 handles.push(scope.spawn(move || {
3602 for (frame, slot) in
3603 frame_slice.iter().zip(dest.chunks_exact_mut(stride))
3604 {
3605 write(frame, slot);
3606 }
3607 }));
3608 base += take_frames;
3609 }
3610 for handle in handles {
3611 handle.join().expect("slab worker");
3612 }
3613 });
3614 }
3615 return Some(values);
3616 }
3617 }
3618 #[cfg(not(feature = "parallel"))]
3619 {
3620 let _ = workers;
3621 }
3622 for (frame, dest) in frames.iter().zip(values.chunks_exact_mut(stride)) {
3623 write(frame, dest);
3624 }
3625 Some(values)
3626}
3627
3628fn mix_touch(hash: u64, value: u64) -> u64 {
3629 hash.wrapping_mul(0x9E3779B185EBCA87) ^ value
3630}
3631
3632fn one_frame_touch(frame: &types::ConFrame, coords: &[f64], index: u64) -> u64 {
3633 let mut hash = index;
3634 for value in frame.header.boxl {
3635 hash = mix_touch(hash, value.to_bits());
3636 }
3637 for atom in &frame.atom_data {
3638 hash = mix_touch(hash, atom.symbol.len() as u64);
3639 for byte in atom.symbol.as_bytes() {
3640 hash = mix_touch(hash, u64::from(*byte));
3641 }
3642 }
3643 for value in coords {
3644 hash = mix_touch(hash, value.to_bits());
3645 }
3646 hash
3647}
3648
3649pub fn touch_frame_checksum(
3656 frames: &[types::ConFrame],
3657 positions: &[f64],
3658 workers: usize,
3659) -> Option<u64> {
3660 if frames.is_empty() {
3661 return Some(0);
3662 }
3663 let n_atoms = frames[0].atom_data.len();
3664 let stride = n_atoms.saturating_mul(3);
3665 if stride == 0 || positions.len() != frames.len().saturating_mul(stride) {
3666 return None;
3667 }
3668 if frames.iter().any(|frame| frame.atom_data.len() != n_atoms) {
3669 return None;
3670 }
3671 let one = |index: usize| {
3672 let start = index * stride;
3673 one_frame_touch(
3674 &frames[index],
3675 &positions[start..start + stride],
3676 index as u64,
3677 )
3678 };
3679 Some(reduce_touch(frames.len(), workers, one))
3680}
3681
3682pub fn touch_option_checksum(
3686 frames: &[Option<types::ConFrame>],
3687 positions: &[f64],
3688 workers: usize,
3689) -> Option<u64> {
3690 if frames.is_empty() {
3691 return Some(0);
3692 }
3693 if frames.iter().any(Option::is_none) {
3694 return None;
3695 }
3696 let n_atoms = frames[0].as_ref()?.atom_data.len();
3697 let stride = n_atoms.saturating_mul(3);
3698 if stride == 0 || positions.len() != frames.len().saturating_mul(stride) {
3699 return None;
3700 }
3701 if frames
3702 .iter()
3703 .any(|frame| frame.as_ref().map(|item| item.atom_data.len()) != Some(n_atoms))
3704 {
3705 return None;
3706 }
3707 let one = |index: usize| {
3708 let start = index * stride;
3709 one_frame_touch(
3710 frames[index].as_ref().expect("slot"),
3711 &positions[start..start + stride],
3712 index as u64,
3713 )
3714 };
3715 Some(reduce_touch(frames.len(), workers, one))
3716}
3717
3718fn reduce_touch(n: usize, workers: usize, one: impl Fn(usize) -> u64 + Sync) -> u64 {
3719 #[cfg(feature = "parallel")]
3720 if workers >= 2 {
3721 let fold_range = |first: usize, last: usize| {
3722 let mut acc = 0u64;
3723 for index in first..last {
3724 acc ^= one(index);
3725 }
3726 acc
3727 };
3728 let chunk = n.div_ceil(workers).max(1);
3729 if rayon::current_thread_index().is_some() {
3732 use rayon::prelude::*;
3733 return (0..workers)
3734 .into_par_iter()
3735 .map(|worker| {
3736 let first = worker * chunk;
3737 let last = (first + chunk).min(n);
3738 fold_range(first, last)
3739 })
3740 .reduce(|| 0u64, |left, right| left ^ right);
3741 }
3742 let mut acc = 0u64;
3743 std::thread::scope(|scope| {
3744 let mut handles = Vec::with_capacity(workers);
3745 for worker in 0..workers {
3746 handles.push(scope.spawn(move || {
3747 let first = worker * chunk;
3748 let last = (first + chunk).min(n);
3749 fold_range(first, last)
3750 }));
3751 }
3752 for handle in handles {
3753 acc ^= handle.join().unwrap_or(0);
3754 }
3755 });
3756 return acc;
3757 }
3758 #[cfg(not(feature = "parallel"))]
3759 {
3760 let _ = workers;
3761 }
3762 (0..n).map(one).fold(0, |acc, item| acc ^ item)
3763}
3764
3765pub fn measure_newline_scan(text: &str, threads: usize, chunked: bool) -> (Vec<usize>, f64) {
3773 #[cfg(feature = "parallel")]
3774 {
3775 if chunked && threads >= 2 {
3776 let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build();
3777 let t_scan = std::time::Instant::now();
3778 let offsets = match pool {
3779 Ok(pool) => pool.install(|| {
3780 let newlines = newline_positions(text.as_bytes(), threads);
3781 frame_start_offsets_indexed(text, newlines)
3782 }),
3783 Err(_) => {
3784 let newlines = newline_positions(text.as_bytes(), threads);
3785 frame_start_offsets_indexed(text, newlines)
3786 }
3787 };
3788 return (offsets, t_scan.elapsed().as_secs_f64());
3789 }
3790 }
3791 #[cfg(not(feature = "parallel"))]
3792 {
3793 let _ = (threads, chunked);
3794 }
3795 let t_scan = std::time::Instant::now();
3796 let offsets = frame_start_offsets_serial(text);
3797 (offsets, t_scan.elapsed().as_secs_f64())
3798}
3799
3800#[cfg(feature = "parallel")]
3803fn collect_parallel_located(
3804 file_contents: &str,
3805 num_threads: Option<usize>,
3806) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
3807 use rayon::prelude::*;
3808
3809 let boundaries = frame_start_offsets(file_contents);
3810 let parse_chunks = || -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
3811 let num_frames = boundaries.len();
3812 let parts: Vec<Result<types::ConFrame, error::LocatedParseError>> = (0..num_frames)
3813 .into_par_iter()
3814 .map(|i| {
3815 let start = boundaries[i];
3816 let end = if i + 1 < num_frames {
3817 boundaries[i + 1]
3818 } else {
3819 file_contents.len()
3820 };
3821 let chunk = &file_contents[start..end];
3822 let mut iter = ConFrameIterator::new(chunk);
3823 match iter.next() {
3824 Some(Ok(frame)) => Ok(frame),
3825 Some(Err(e)) => {
3826 let local = iter.lines.line_no();
3827 let base = file_line_at(file_contents, start);
3828 let cursor = if local == 0 { base } else { base + local - 1 };
3829 Err(error::LocatedParseError::new(e, cursor, i as u64))
3830 }
3831 None => Err(error::LocatedParseError::new(
3832 error::ParseError::IncompleteFrame,
3833 file_line_at(file_contents, start),
3834 i as u64,
3835 )),
3836 }
3837 })
3838 .collect();
3839 let mut frames = Vec::with_capacity(parts.len());
3840 for part in parts {
3841 frames.push(part?);
3842 }
3843 Ok(frames)
3844 };
3845 match num_threads {
3846 None => parse_chunks(),
3847 Some(n) => {
3848 let n = n.max(1);
3849 match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
3850 Ok(pool) => pool.install(parse_chunks),
3851 Err(_) => parse_chunks(),
3852 }
3853 }
3854 }
3855}
3856
3857#[cfg(all(test, feature = "parallel"))]
3858mod parallel_strong_scale_tests {
3859 use super::*;
3860 use std::path::PathBuf;
3861
3862 fn multi_frame_fixture() -> String {
3863 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con");
3864 let one = std::fs::read_to_string(p).expect("fixture");
3865 one.repeat(8)
3867 }
3868
3869 fn sequential_frames(text: &str) -> Vec<types::ConFrame> {
3870 ConFrameIterator::new(text)
3871 .map(|r| r.expect("seq frame"))
3872 .collect()
3873 }
3874
3875 fn frames_payload_key(f: &types::ConFrame) -> (usize, Vec<(String, f64, f64, f64)>) {
3876 let atoms: Vec<_> = f
3877 .atom_data
3878 .iter()
3879 .map(|a| (a.symbol.to_string(), a.x, a.y, a.z))
3880 .collect();
3881 (f.atom_data.len(), atoms)
3882 }
3883
3884 #[test]
3885 fn parallel_workers_match_sequential_payloads() {
3886 let text = multi_frame_fixture();
3887 let seq = sequential_frames(&text);
3888 assert!(seq.len() >= 8);
3889 let seq_keys: Vec<_> = seq.iter().map(frames_payload_key).collect();
3890
3891 for workers in [1usize, 2, 4] {
3892 let par = parse_frames_parallel_with_threads(&text, Some(workers));
3893 assert_eq!(par.len(), seq.len(), "workers={workers}");
3894 let par_keys: Vec<_> = par
3895 .into_iter()
3896 .map(|r| frames_payload_key(&r.expect("par frame")))
3897 .collect();
3898 assert_eq!(par_keys, seq_keys, "workers={workers} frame payloads");
3899 }
3900
3901 let par_default = parse_frames_parallel(&text);
3903 let def_keys: Vec<_> = par_default
3904 .into_iter()
3905 .map(|r| frames_payload_key(&r.expect("par")))
3906 .collect();
3907 assert_eq!(def_keys, seq_keys);
3908 }
3909
3910 fn live_threads() -> usize {
3911 std::fs::read_to_string("/proc/self/status")
3912 .unwrap()
3913 .lines()
3914 .find(|line| line.starts_with("Threads:"))
3915 .unwrap()
3916 .split_whitespace()
3917 .nth(1)
3918 .unwrap()
3919 .parse()
3920 .unwrap()
3921 }
3922
3923 #[test]
3925 fn default_read_of_a_wide_file_stays_on_the_calling_thread() {
3926 const CHILD: &str = "READCON_DEFAULT_THREAD_CHILD";
3927 const NAME: &str = "iterators::parallel_strong_scale_tests::default_read_of_a_wide_file_stays_on_the_calling_thread";
3928 if std::env::var_os(CHILD).is_none() {
3929 let out = std::process::Command::new(std::env::current_exe().unwrap())
3930 .args([NAME, "--exact", "--test-threads=1"])
3931 .env(CHILD, "1")
3932 .output()
3933 .unwrap();
3934 let stdout = String::from_utf8_lossy(&out.stdout);
3935 assert!(
3936 out.status.success() && stdout.contains("test result: ok. 1 passed;"),
3937 "{stdout}{}",
3938 String::from_utf8_lossy(&out.stderr)
3939 );
3940 return;
3941 }
3942 let dir = tempfile::tempdir().unwrap();
3943 let path = dir.path().join("wide.con");
3944 let one = std::fs::read_to_string(
3945 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con"),
3946 )
3947 .unwrap();
3948 std::fs::write(&path, one.repeat(200)).unwrap();
3949 assert!(std::fs::metadata(&path).unwrap().len() > 64 * 1024);
3950 let before = live_threads();
3951 let frames = super::read_all_frames(&path).unwrap();
3952 let after = live_threads();
3953 assert_eq!(before, after, "default read started worker threads");
3954 assert!(frames.len() > 1);
3955 let text = std::fs::read_to_string(&path).unwrap();
3956 let sequential = super::frames_from_text(&text, None).unwrap();
3957 assert_eq!(sequential.len(), frames.len());
3958 assert_eq!(
3959 sequential[0].atom_data[0].x, frames[0].atom_data[0].x,
3960 "default read changed a coordinate"
3961 );
3962 }
3963}
3964
3965#[cfg(all(test, feature = "parallel"))]
3966mod parallel_header_scan_tests {
3967 use super::*;
3968 use std::path::PathBuf;
3969
3970 fn fixture(name: &str) -> String {
3971 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3972 .join("resources/test")
3973 .join(name);
3974 std::fs::read_to_string(path).expect(name)
3975 }
3976
3977 fn assert_crlf_gap_cut(text: &str) {
3979 let bytes = text.as_bytes();
3980 let mut found = 0usize;
3981 let mut i = 0usize;
3982 while i + 1 < bytes.len() && found < 6 {
3983 if bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
3984 let cut = i + 1;
3985 assert_eq!(bytes[cut - 1], b'\r');
3986 assert_eq!(bytes[cut], b'\n');
3987 assert_cuts_match(text, &[0, cut, bytes.len()]);
3988 found += 1;
3989 i += 2;
3990 continue;
3991 }
3992 i += 1;
3993 }
3994 assert!(found > 0, "fixture has no CRLF pair");
3995 }
3996
3997 fn assert_cuts_match(text: &str, cuts: &[usize]) {
3998 let serial_nl: Vec<usize> = memchr::memchr_iter(b'\n', text.as_bytes()).collect();
3999 let stitched = newline_positions_at_cuts(text.as_bytes(), cuts);
4000 assert_eq!(stitched, serial_nl, "newline stitch at {cuts:?}");
4001 let serial = frame_start_offsets_serial(text);
4002 let parallel = frame_start_offsets_at_cuts(text, cuts);
4003 assert_eq!(parallel, serial, "frame offsets at {cuts:?}");
4004 }
4005
4006 fn header_straddle_cuts(text: &str) -> Vec<Vec<usize>> {
4007 let serial = frame_start_offsets_serial(text);
4008 let bytes = text.as_bytes();
4009 let mut out = Vec::new();
4010 for n in [2usize, 3, 4, 8] {
4012 out.push(equal_cuts(bytes.len(), n));
4013 }
4014 for (index, &start) in serial.iter().enumerate() {
4015 let next = serial.get(index + 1).copied().unwrap_or(bytes.len());
4016 let window = super::header_window_end(bytes, start, next);
4019 let mut line = start;
4020 while line < window {
4021 let Some(rel) = memchr::memchr(b'\n', &bytes[line..window]) else {
4022 break;
4023 };
4024 let end = line + rel;
4025 if end - line > 1 {
4026 let mid = line + (end - line) / 2;
4027 assert!(mid > line && mid < end);
4029 out.push(vec![0, mid, bytes.len()]);
4030 }
4031 line = end + 1;
4032 }
4033 }
4034 out
4035 }
4036
4037 fn assert_slab(text: &str) {
4038 assert!(!text.is_empty());
4039 for cuts in header_straddle_cuts(text) {
4040 assert_cuts_match(text, &cuts);
4041 }
4042 let bare = text.trim_end_matches(['\n', '\r']);
4043 if !bare.is_empty() {
4044 assert_cuts_match(bare, &equal_cuts(bare.len(), 4));
4045 let mid = bare.len() / 2;
4046 let line_start = bare[..mid].rfind('\n').map(|i| i + 1).unwrap_or(0);
4047 let line_end = bare[mid..]
4048 .find('\n')
4049 .map(|i| mid + i)
4050 .unwrap_or(bare.len());
4051 if line_end - line_start > 1 {
4052 let cut = line_start + (line_end - line_start) / 2;
4053 assert_cuts_match(bare, &[0, cut, bare.len()]);
4054 }
4055 }
4056 let tailed = format!("{text}\n \n\t\n");
4057 assert_cuts_match(
4058 &tailed,
4059 &[0, 3, tailed.len() / 2, tailed.len() - 1, tailed.len()],
4060 );
4061 let crlf = text.replace('\n', "\r\n");
4062 assert_cuts_match(&crlf, &equal_cuts(crlf.len(), 5));
4063 assert_crlf_gap_cut(&crlf);
4064 let serial = frame_start_offsets_serial(text);
4065 if serial.len() >= 2 {
4066 let second = serial[1];
4067 let next = serial.get(2).copied().unwrap_or(text.len());
4068 let broken_at = second + (next - second) / 2;
4069 let broken = &text[..broken_at.max(1)];
4070 assert_cuts_match(broken, &[0, 1.max(broken.len() / 3), broken.len()]);
4071 }
4072 }
4073
4074 #[test]
4075 fn chunked_header_scan_matches_serial_offsets() {
4076 assert_eq!(frame_start_offsets_serial(""), frame_start_offsets(""));
4077 assert_cuts_match("\n", &[0, 1]);
4078 assert_cuts_match(" \n\n", &[0, 2, 5]);
4079 for name in [
4080 "tiny_cuh2.con",
4081 "tiny_cuh2.convel",
4082 "tiny_cuh2_forces.con",
4083 "tiny_cuh2_vel_forces.con",
4084 "cuh2.con",
4085 ] {
4086 let one = fixture(name);
4087 assert_slab(&one);
4088 assert_slab(&one.repeat(3));
4089 }
4090 let one = fixture("cuh2.con");
4094 for wide in [one.clone(), one.repeat(8)] {
4095 let serial = frame_start_offsets_serial(&wide);
4096 for workers in [2usize, 3, 4, 8] {
4097 let parallel = frame_start_offsets_with_threads(&wide, Some(workers));
4098 assert_eq!(parallel, serial, "workers={workers} bytes={}", wide.len());
4099 let parsed =
4100 frames_from_bytes(wide.as_bytes(), Some(workers)).expect("bytes parse");
4101 assert_eq!(parsed.len(), serial.len());
4102 let seq = frames_from_text(&wide, Some(1)).expect("serial parse");
4103 assert_eq!(parsed.len(), seq.len());
4104 assert_eq!(parsed[0].atom_data[0].x, seq[0].atom_data[0].x);
4105 assert_eq!(
4106 parsed.last().unwrap().atom_data.last().unwrap().z,
4107 seq.last().unwrap().atom_data.last().unwrap().z
4108 );
4109 }
4110 }
4111 }
4112}
4113
4114#[cfg(all(test, feature = "parallel"))]
4119fn header_window_end(bytes: &[u8], start: usize, limit: usize) -> usize {
4120 let limit = limit.min(bytes.len());
4121 let marker = b"Coordinates";
4122 let mut line = start;
4123 while line < limit {
4124 let rest = &bytes[line..limit];
4125 let rel = memchr::memchr(b'\n', rest).unwrap_or(rest.len());
4126 let content_end = line + rel;
4127 let content = &bytes[line..content_end];
4128 let trimmed = match content.iter().position(|byte| !byte.is_ascii_whitespace()) {
4129 Some(index) => &content[index..],
4130 None => b"",
4131 };
4132 if trimmed.starts_with(marker) {
4133 return line;
4134 }
4135 if content_end >= limit {
4136 break;
4137 }
4138 line = content_end + 1;
4139 }
4140 limit
4141}
4142
4143#[cfg(all(test, feature = "parallel"))]
4144mod parallel_frame_parse_tests {
4145 use super::*;
4146 use std::path::PathBuf;
4147
4148 const ZERO_ATOM: &str = "\
4149zero-atom frame
4150{\"con_spec_version\":3,\"con_spec_minor\":1,\"units\":{\"length\":\"angstrom\",\"energy\":\"eV\"}}
415110.0 10.0 10.0
415290.0 90.0 90.0
4153
4154
41550
4156
4157
4158";
4159
4160 fn fixture(name: &str) -> String {
4161 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
4162 .join("resources/test")
4163 .join(name);
4164 std::fs::read_to_string(path).expect(name)
4165 }
4166
4167 fn serial_results(text: &str) -> Vec<Result<types::ConFrame, error::ParseError>> {
4168 ConFrameIterator::new(text).collect()
4169 }
4170
4171 fn written(frame: &types::ConFrame) -> Vec<u8> {
4172 let mut out = Vec::new();
4173 {
4174 let mut writer = crate::writer::ConFrameWriter::new(&mut out);
4175 writer.write_frame(frame).expect("write");
4176 writer.flush().expect("flush");
4177 }
4178 out
4179 }
4180
4181 fn assert_same_results(
4182 label: &str,
4183 serial: Vec<Result<types::ConFrame, error::ParseError>>,
4184 parallel: Vec<Result<types::ConFrame, error::ParseError>>,
4185 ) {
4186 assert_eq!(serial.len(), parallel.len(), "{label} count");
4187 for (i, (left, right)) in serial.into_iter().zip(parallel).enumerate() {
4188 match (left, right) {
4189 (Ok(a), Ok(b)) => {
4190 assert_eq!(written(&a), written(&b), "{label} bytes frame {i}");
4191 assert_eq!(a, b, "{label} fields frame {i}");
4192 }
4193 (Err(a), Err(b)) => {
4194 assert_eq!(
4195 format!("{a:?}"),
4196 format!("{b:?}"),
4197 "{label} error frame {i}"
4198 );
4199 }
4200 (Ok(_), Err(err)) => panic!("{label} frame {i} serial ok parallel {err:?}"),
4201 (Err(err), Ok(_)) => panic!("{label} frame {i} serial {err:?} parallel ok"),
4202 }
4203 }
4204 }
4205
4206 fn assert_workers(label: &str, text: &str) {
4207 let serial_ok = serial_results(text).iter().all(|item| item.is_ok());
4208 for workers in [1usize, 2, 3, 4, 8] {
4209 let parallel = parse_frames_parallel_with_threads(text, Some(workers));
4210 assert_same_results(
4211 &format!("{label} workers {workers}"),
4212 serial_results(text),
4213 parallel,
4214 );
4215 let bounds = collect_frame_starts(ConFrameIterator::new(text), true);
4216 let at = parse_frames_at_offsets(text, &bounds, Some(workers));
4217 assert_same_results(
4218 &format!("{label} offsets workers {workers}"),
4219 serial_results(text),
4220 at,
4221 );
4222 }
4223 if serial_ok {
4224 for workers in [2usize, 3, 8] {
4225 let bytes = frames_from_bytes(text.as_bytes(), Some(workers)).expect(label);
4226 let seq = frames_from_text(text, Some(1)).expect(label);
4227 assert_eq!(bytes.len(), seq.len(), "{label} byte count");
4228 for (i, (a, b)) in bytes.iter().zip(seq.iter()).enumerate() {
4229 assert_eq!(written(a), written(b), "{label} byte path frame {i}");
4230 assert_eq!(a, b, "{label} byte path fields frame {i}");
4231 }
4232 }
4233 }
4234 }
4235
4236 fn crlf_gap_offsets(text: &str) -> Vec<usize> {
4237 let bytes = text.as_bytes();
4238 let mut cut = None;
4239 let mut i = 0usize;
4240 while i + 1 < bytes.len() {
4241 if bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
4242 cut = Some(i + 1);
4243 break;
4244 }
4245 i += 1;
4246 }
4247 let cut = cut.expect("crlf");
4248 frame_start_offsets_at_cuts(text, &[0, cut, bytes.len()])
4249 }
4250
4251 #[test]
4252 fn parallel_slots_match_serial_frames() {
4253 assert_workers("empty", "");
4254 assert_workers("blank", "\n");
4255 assert_workers("space", " \n\n");
4256 assert_workers("zero", ZERO_ATOM);
4257 assert_workers("zero-repeat", &ZERO_ATOM.repeat(5));
4258 for name in [
4259 "tiny_cuh2.con",
4260 "tiny_cuh2.convel",
4261 "tiny_cuh2_forces.con",
4262 "tiny_cuh2_vel_forces.con",
4263 "tiny_cuh2_charges_spins_magmoms.con",
4264 "tiny_cuh2_displacements.con",
4265 "tiny_cuh2_spreads.con",
4266 "cuh2.con",
4267 ] {
4268 let one = fixture(name);
4269 assert_workers(name, &one);
4270 assert_workers(&format!("{name} x5"), &one.repeat(5));
4271 let bare = one.trim_end_matches(['\n', '\r']);
4272 assert_workers(&format!("{name} bare"), bare);
4273 let tailed = format!("{one}\n \n\t\n");
4274 assert_workers(&format!("{name} tail"), &tailed);
4275 let crlf = one.replace('\n', "\r\n");
4276 assert_workers(&format!("{name} crlf"), &crlf);
4277 let gap = crlf_gap_offsets(&crlf);
4278 assert_eq!(
4279 gap,
4280 frame_start_offsets_serial(&crlf),
4281 "{name} crlf gap offsets"
4282 );
4283 let parsed = parse_frames_at_offsets(&crlf, &gap, Some(4));
4284 assert_same_results(
4285 &format!("{name} crlf gap parse"),
4286 serial_results(&crlf),
4287 parsed,
4288 );
4289 let serial_offs = frame_start_offsets_serial(&one);
4290 let broken: &str = if serial_offs.len() >= 2 {
4291 let second = serial_offs[1];
4292 let next = serial_offs.get(2).copied().unwrap_or(one.len());
4293 &one[..second + (next - second) / 2]
4294 } else if one.len() > 8 {
4295 &one[..one.len() / 2]
4296 } else {
4297 ""
4298 };
4299 if !broken.is_empty() {
4300 let serial_err = frames_from_text(broken, Some(1))
4301 .err()
4302 .map(|e| format!("{e:?}"));
4303 assert!(serial_err.is_some(), "{name} chopped still parsed");
4304 for workers in [1usize, 2, 4, 8] {
4305 let parallel_err = frames_from_text(broken, Some(workers))
4306 .err()
4307 .map(|e| format!("{e:?}"));
4308 assert_eq!(parallel_err, serial_err, "{name} chopped workers {workers}");
4309 let byte_err = frames_from_bytes(broken.as_bytes(), Some(workers))
4310 .err()
4311 .map(|e| format!("{e:?}"));
4312 assert_eq!(
4313 byte_err, serial_err,
4314 "{name} chopped bytes workers {workers}"
4315 );
4316 }
4317 }
4318 }
4319 }
4320}
4321
4322#[cfg(all(test, feature = "parallel"))]
4323mod phased_read_tests {
4324 use super::*;
4325 use std::path::PathBuf;
4326
4327 fn fixture(name: &str) -> String {
4328 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
4329 .join("resources/test")
4330 .join(name);
4331 std::fs::read_to_string(path).expect(name)
4332 }
4333
4334 #[test]
4335 fn phased_bytes_match_serial_frames() {
4336 let text = fixture("cuh2.con").repeat(4);
4337 let serial = frames_from_text(&text, Some(1)).expect("serial");
4338 for workers in [1usize, 2, 4, 8] {
4339 let phased = frames_from_bytes_phased(text.as_bytes(), Some(workers)).expect("phased");
4340 let parallel = phased.frames;
4341 assert_eq!(parallel.len(), serial.len(), "workers={workers}");
4342 assert!(
4343 phased.scan_s >= 0.0 && phased.slot_alloc_s >= 0.0 && phased.worker_parse_s >= 0.0
4344 );
4345 assert_eq!(parallel[0], serial[0], "workers={workers} first");
4346 assert_eq!(
4347 parallel.last().unwrap(),
4348 serial.last().unwrap(),
4349 "workers={workers} last"
4350 );
4351 let slab = pack_position_slab(&serial, workers).expect("slab");
4352 let stride = serial[0].positions.nrows() * 3;
4353 let src = serial[0].positions.f64_slice().expect("f64");
4354 assert_eq!(&slab[..stride], src);
4355 let (chunked, _) = measure_newline_scan(&text, workers.max(2), true);
4356 let (plain, _) = measure_newline_scan(&text, 1, false);
4357 if workers >= 2 {
4358 assert_eq!(chunked, plain);
4359 }
4360 }
4361 }
4362
4363 #[test]
4364 fn phased_file_matches_serial_and_maps_a_wide_file() {
4365 let text = fixture("cuh2.con").repeat(5);
4366 let dir = std::env::temp_dir();
4367 let path = dir.join("readcon-phased-wide.con");
4368 std::fs::write(&path, &text).expect("write");
4369 let phased = read_all_frames_phased(&path, Some(4)).expect("phased");
4370 let serial = frames_from_text(&text, Some(1)).expect("serial");
4371 assert_eq!(phased.frames, serial);
4372 assert!(phased.phases.mapped, "a file past 64 KiB is mapped");
4373 assert!(phased.phases.worker_parse_s >= 0.0);
4374 assert_eq!(phased.n_bytes, text.len());
4375 let _ = std::fs::remove_file(&path);
4376 }
4377
4378 #[test]
4379 fn equal_span_boundaries_match_the_serial_walk() {
4380 let text = fixture("cuh2.con").repeat(8);
4381 let bytes = text.as_bytes();
4382 let got = equal_span_boundaries(bytes, 4).expect("equal span");
4383 let expect = collect_frame_starts(ConFrameIterator::from_bytes(bytes), true);
4384 assert_eq!(got, expect);
4385 assert_eq!(got.len(), 8);
4386 let phased = frames_from_bytes_phased(bytes, Some(4)).expect("phased");
4387 assert_eq!(phased.scan_kind, SCAN_EQUAL_SPAN);
4388 assert_eq!(
4389 phased.frames,
4390 frames_from_text(&text, Some(1)).expect("serial")
4391 );
4392 let tailed = format!("{text}\n\n \n");
4393 let tail_phased = frames_from_bytes_phased(tailed.as_bytes(), Some(4)).expect("tail");
4394 assert_eq!(tail_phased.scan_kind, SCAN_EQUAL_SPAN);
4395 assert_eq!(
4396 tail_phased.frames,
4397 frames_from_text(&tailed, Some(1)).expect("tail serial")
4398 );
4399 }
4400
4401 #[test]
4402 fn a_whitespace_tail_uses_the_equal_span_walk() {
4403 let mut text = fixture("cuh2.con").repeat(3);
4404 text.push('\n');
4405 assert!(equal_span_boundaries(text.as_bytes(), 4).is_some());
4406 let serial = frames_from_text(&text, Some(1)).expect("serial");
4407 let parallel = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("phased");
4408 assert_eq!(parallel.scan_kind, SCAN_EQUAL_SPAN);
4409 assert_eq!(parallel.frames, serial);
4410 }
4411
4412 #[test]
4413 fn touch_checksum_matches_across_worker_counts() {
4414 let text = fixture("cuh2.con").repeat(4);
4415 let serial = frames_from_bytes_phased(text.as_bytes(), Some(1)).expect("serial");
4416 let parallel = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("parallel");
4417 let slab_s = pack_position_slab(&serial.frames, 1).expect("slab");
4418 let slab_p = pack_position_slab(¶llel.frames, 4).expect("slab");
4419 let left = touch_frame_checksum(&serial.frames, &slab_s, 1).expect("touch");
4420 let right = touch_frame_checksum(¶llel.frames, &slab_p, 4).expect("touch");
4421 assert_eq!(left, right);
4422 assert_ne!(left, 0);
4423 }
4424}
4425
4426#[cfg(test)]
4427mod later_frame_location_tests {
4428 use super::*;
4429 use std::fs;
4430
4431 fn one_atom(coord: &str) -> String {
4432 format!(
4433 "seed\n{{\"con_spec_version\":2}}\n10 10 10\n90 90 90\n0 0\n0 0 0\n1\n1\n1.0\nH\nCoordinates of Component 1\n{coord}\n"
4434 )
4435 }
4436
4437 fn bad_count_frame() -> String {
4438 "seed\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\n0 0\n0 0 0\n1\n-1\n1.0\nH\nCoordinates of Component 1\n0 0 0 0 0\n".into()
4439 }
4440
4441 fn two_frames() -> String {
4442 format!("{}{}", one_atom("0 0 0 0 0"), bad_count_frame())
4443 }
4444
4445 fn assert_later_count(
4446 err: &error::ParseError,
4447 line: u32,
4448 frame: u64,
4449 iter: &ConFrameIterator<'_>,
4450 ) {
4451 assert_eq!(err.class_name(), "InvalidCount", "{err}");
4452 assert_eq!(iter.lines.line_no(), line, "{err}");
4453 assert_eq!(iter.frame_index(), frame, "{err}");
4454 }
4455
4456 fn assert_first_pass(err: Box<dyn std::error::Error>) {
4457 let located = err
4458 .downcast_ref::<error::LocatedParseError>()
4459 .unwrap_or_else(|| panic!("first pass lost the location: {err}"));
4460 assert_eq!(located.error.class_name(), "InvalidCount", "{located:?}");
4461 assert_eq!(located.line, 20, "{located:?}");
4462 assert_eq!(located.frame_index, 1, "{located:?}");
4463 }
4464
4465 fn take_err<T>(result: Result<T, Box<dyn std::error::Error>>) -> Box<dyn std::error::Error> {
4466 match result {
4467 Err(err) => err,
4468 Ok(_) => panic!("expected a parse error"),
4469 }
4470 }
4471
4472 #[test]
4475 fn batch_readers_keep_the_later_frame_on_the_first_pass() {
4476 let path = std::env::temp_dir().join(format!(
4477 "readcon-later-frame-first-{}.con",
4478 std::process::id()
4479 ));
4480 fs::write(&path, two_frames()).unwrap();
4481 for threads in [None, Some(1), Some(2)] {
4482 assert_first_pass(take_err(read_all_frames_with_threads(&path, threads)));
4483 assert_first_pass(take_err(read_all_frames_phased(&path, threads)));
4484 assert_first_pass(take_err(read_positions_with_threads(&path, threads)));
4485 }
4486 let _ = fs::remove_file(&path);
4487 }
4488
4489 #[test]
4491 fn next_forward_and_skip_locate_the_later_frame() {
4492 let text = two_frames();
4493
4494 let mut iter = ConFrameIterator::new(&text);
4495 assert!(iter.next().unwrap().is_ok());
4496 let err = iter.next().unwrap().unwrap_err();
4497 assert_later_count(&err, 20, 1, &iter);
4498
4499 let mut iter = ConFrameIterator::new(&text);
4500 assert!(iter.forward().unwrap().is_ok());
4501 let err = iter.forward().unwrap().unwrap_err();
4502 assert_later_count(&err, 20, 1, &iter);
4503
4504 let mut iter = ConFrameIterator::new(&text);
4505 let err = iter.skip_frames(3).unwrap_err();
4506 assert_later_count(&err, 20, 1, &iter);
4507 }
4508
4509 #[test]
4513 fn skip_after_a_velocity_frame_keeps_the_separator_line() {
4514 let text = format!(
4515 "{}\nH\nVelocities of Component 1\n0 0 0 0 0\n{}",
4516 one_atom("0 0 0 0 0"),
4517 bad_count_frame()
4518 );
4519 let mut iter = ConFrameIterator::new(&text);
4520 assert!(iter.forward().unwrap().is_ok());
4521 let err = iter.forward().unwrap().unwrap_err();
4522 assert_later_count(&err, 24, 1, &iter);
4523 }
4524
4525 #[test]
4526 fn count_frames_locates_the_later_frame() {
4527 let path = std::env::temp_dir().join(format!(
4528 "readcon-later-frame-count-{}.con",
4529 std::process::id()
4530 ));
4531 fs::write(&path, two_frames()).unwrap();
4532 let err = count_frames(&path).unwrap_err();
4533 let _ = fs::remove_file(&path);
4534 match err {
4535 LoadError::Parse(located) => {
4536 assert_eq!(located.error.class_name(), "InvalidCount");
4537 assert_eq!(located.line, 20, "{located}");
4538 assert_eq!(located.frame_index, 1, "{located}");
4539 }
4540 other => panic!("expected a located parse error, got {other}"),
4541 }
4542 }
4543
4544 #[test]
4547 fn metadata_in_a_later_frame_reports_that_frames_line() {
4548 let bad = "seed\n{not-json\n10 10 10\n90 90 90\n0 0\n0 0 0\n1\n1\n1.0\nH\nCoordinates of Component 1\n0 0 0 0 0\n";
4549 let text = format!("{}{}", one_atom("0 0 0 0 0"), bad);
4550 for threads in [Some(1usize), Some(2)] {
4551 let err = frames_from_text_located(&text, threads).unwrap_err();
4552 assert_eq!(
4553 err.error.class_name(),
4554 "InvalidMetadataJson",
4555 "{threads:?}: {err}"
4556 );
4557 assert_eq!(err.frame_index, 1, "{threads:?}");
4558 assert_eq!(err.line, 14, "{threads:?}: {err}");
4559 }
4560 }
4561
4562 #[test]
4570 fn parallel_positions_report_the_earliest_failing_slot() {
4571 const SLOW_ATOMS: usize = 20_000;
4572 let slow = counted_frame(SLOW_ATOMS);
4573 let bad = counted_frame(1);
4574 let text = format!("{slow}{bad}{bad}{bad}");
4575 let earliest_line = 1 + slow.bytes().filter(|byte| *byte == b'\n').count() as u32;
4576 let path =
4577 std::env::temp_dir().join(format!("readcon-earliest-slot-{}.con", std::process::id()));
4578 fs::write(&path, &text).unwrap();
4579 assert_earliest_slot(
4580 take_err(read_positions_with_threads(&path, Some(1))),
4581 earliest_line,
4582 );
4583 assert_earliest_slot(
4584 take_err(read_positions_with_threads(&path, Some(2))),
4585 earliest_line,
4586 );
4587 let _ = fs::remove_file(&path);
4588 }
4589
4590 fn counted_frame(natoms: usize) -> String {
4591 let mut body = String::with_capacity(natoms * 10 + 96);
4592 body.push_str("seed\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\n0 0\n0 0 0\n1\n");
4593 body.push_str(&natoms.to_string());
4594 body.push_str("\n1.0\nH\nCoordinates of Component 1\n");
4595 for _ in 0..natoms {
4596 body.push_str("0 0 0 0 0\n");
4597 }
4598 body
4599 }
4600
4601 fn assert_earliest_slot(err: Box<dyn std::error::Error>, line: u32) {
4602 let located = err
4603 .downcast_ref::<error::LocatedParseError>()
4604 .unwrap_or_else(|| panic!("position read lost the location: {err}"));
4605 assert_eq!(located.error.class_name(), "InvalidCount", "{located:?}");
4606 assert_eq!(located.frame_index, 1, "{located:?}");
4607 assert_eq!(located.line, line, "{located:?}");
4608 }
4609}
4610
4611#[cfg(test)]
4612mod frame_equality_tests {
4613 use super::ConFrameIterator;
4614
4615 fn sample_con(frames: usize, long: bool) -> String {
4616 let mut out = String::new();
4617 for f in 0..frames {
4618 let shift = (f as f64) * 1.0e-4;
4619 out.push_str("Generated by benchmark\n");
4620 out.push_str("{\"con_spec_version\":2}\n");
4621 out.push_str("15.3456 21.702 100.0\n");
4622 out.push_str("90.0 90.0 90.0\n");
4623 out.push_str("0 0\n");
4624 out.push_str(&format!("{f} 0 1\n"));
4625 out.push_str("2\n6 2\n63.546 1.00793\n");
4626 out.push_str("Cu\nCoordinates of Component 1\n");
4627 for i in 0..6 {
4628 let x = (i as f64) * 2.557 + 0.6394 + shift;
4629 let y = 0.9045 + shift * 0.5;
4630 let z = (f as f64) * 1.0e-6 + if i == 3 { -1.25 } else { 0.0 };
4631 if long {
4632 out.push_str(&format!(" {x:.17} {y:.17} {z:.17} 0 {i}\n"));
4633 } else {
4634 out.push_str(&format!(" {x:.6} {y:.6} {z:.6} 0 {i}\n"));
4635 }
4636 }
4637 out.push_str("H\nCoordinates of Component 2\n");
4638 out.push_str(" 8.6823 9.947 11.733 0 6\n");
4639 out.push_str(&format!(" -7.9421 9.947 {shift:.6} 0 7\n"));
4640 }
4641 out
4642 }
4643
4644 fn sample_convel() -> String {
4645 let mut out = String::new();
4646 for f in 0..3 {
4647 let shift = (f as f64) * 1.0e-4;
4648 out.push_str("Generated by benchmark\n");
4649 out.push_str("{\"con_spec_version\":2}\n");
4650 out.push_str("15.3456 21.702 100.0\n");
4651 out.push_str("90.0 90.0 90.0\n");
4652 out.push_str("0 0\n");
4653 out.push_str(&format!("{f} 0 1\n"));
4654 out.push_str("2\n4 1\n63.546 1.00793\n");
4655 out.push_str("Cu\nCoordinates of Component 1\n");
4656 for i in 0..4 {
4657 let x = (i as f64) * 2.557 + shift;
4658 out.push_str(&format!(" {x:.17} 0.9045 1.0 0 {i}\n"));
4659 }
4660 out.push_str("H\nCoordinates of Component 2\n");
4661 out.push_str(" 1.5 2.5 3.5 0 4\n");
4662 out.push_str("\nCu\nVelocities of Component 1\n");
4663 for i in 0..4 {
4664 let vx = (i as f64) * 1.0e-4 - shift;
4665 out.push_str(&format!(" {vx:.17} -0.5 0.25 0 {i}\n"));
4666 }
4667 out.push_str("H\nVelocities of Component 2\n");
4668 out.push_str(" 0.1 -0.2 0.3 0 4\n");
4669 }
4670 out
4671 }
4672
4673 fn reference_f64(token: &str) -> f64 {
4674 let (val, n) = fast_float2::parse_partial::<f64, _>(token).expect(token);
4675 assert_eq!(n, token.len(), "{token}");
4676 val
4677 }
4678
4679 fn assert_rows_match(text: &str, positions: bool) {
4680 let frames: Vec<_> = ConFrameIterator::new(text)
4681 .map(|frame| frame.expect("parse"))
4682 .collect();
4683 assert!(frames.len() >= 2);
4684 let mut frame_i = 0usize;
4685 let mut row = 0usize;
4686 let mut in_section = false;
4687 for line in text.lines() {
4688 let token = line.trim();
4689 if token.is_empty() {
4690 in_section = true;
4691 row = 0;
4692 continue;
4693 }
4694 if token.starts_with("Generated") {
4695 if row > 0 || in_section {
4696 frame_i += 1;
4697 }
4698 row = 0;
4699 in_section = false;
4700 continue;
4701 }
4702 let cols: Vec<&str> = token.split_whitespace().collect();
4703 if cols.len() < 4 || reference_ok(cols[0]).is_none() {
4704 continue;
4705 }
4706 if cols[3].parse::<u64>().is_err() {
4707 continue;
4708 }
4709 let got = if in_section {
4710 frames[frame_i].atom_data[row].velocity().expect("velocity")
4711 } else if positions {
4712 let p = frames[frame_i].positions.as_f64_row(row);
4713 [p[0], p[1], p[2]]
4714 } else {
4715 continue;
4716 };
4717 for (k, token) in cols.iter().take(3).enumerate() {
4718 assert_eq!(
4719 got[k].to_bits(),
4720 reference_f64(token).to_bits(),
4721 "frame {frame_i} row {row} col {k} token {token} section {in_section}"
4722 );
4723 }
4724 let aos = if in_section {
4725 frames[frame_i].atom_data[row].velocity().unwrap()
4726 } else {
4727 let atom = &frames[frame_i].atom_data[row];
4728 [atom.x, atom.y, atom.z]
4729 };
4730 for k in 0..3 {
4731 assert_eq!(aos[k].to_bits(), got[k].to_bits());
4732 }
4733 row += 1;
4734 }
4735 }
4736
4737 fn reference_ok(token: &str) -> Option<f64> {
4738 let (val, n) = fast_float2::parse_partial::<f64, _>(token).ok()?;
4739 (n == token.len()).then_some(val)
4740 }
4741
4742 #[test]
4743 fn frames_match_general_decimal_parser() {
4744 assert_rows_match(&sample_con(4, true), true);
4745 assert_rows_match(&sample_con(4, false), true);
4746 let text = sample_convel();
4747 let frames: Vec<_> = ConFrameIterator::new(&text)
4748 .map(|frame| frame.expect("convel"))
4749 .collect();
4750 assert_eq!(frames.len(), 3);
4751 assert!(
4752 frames
4753 .iter()
4754 .all(|frame| frame.atom_data[0].velocity().is_some())
4755 );
4756 assert_eq!(frames[0].positions.nrows(), 5);
4757 let mut frame_i = 0usize;
4759 let mut coord_row = 0usize;
4760 let mut vel_row = 0usize;
4761 let mut velocities = false;
4762 for line in text.lines() {
4763 let token = line.trim();
4764 if token.starts_with("Generated") && (coord_row > 0 || velocities) {
4765 frame_i += 1;
4766 coord_row = 0;
4767 vel_row = 0;
4768 velocities = false;
4769 }
4770 if token.starts_with("Velocities") {
4771 velocities = true;
4772 continue;
4773 }
4774 let cols: Vec<&str> = token.split_whitespace().collect();
4775 if cols.len() < 5 {
4776 continue;
4777 }
4778 if reference_ok(cols[0]).is_none() || cols[3].parse::<u64>().is_err() {
4779 continue;
4780 }
4781 let row = if velocities { vel_row } else { coord_row };
4782 let got = if velocities {
4783 frames[frame_i].atom_data[row].velocity().unwrap()
4784 } else {
4785 let p = frames[frame_i].positions.as_f64_row(row);
4786 [p[0], p[1], p[2]]
4787 };
4788 for (k, col) in cols.iter().take(3).enumerate() {
4789 assert_eq!(got[k].to_bits(), reference_f64(col).to_bits(), "{token}");
4790 }
4791 if velocities {
4792 let soa = frames[frame_i].velocities.as_f64_row(row);
4793 for k in 0..3 {
4794 assert_eq!(soa[k].to_bits(), got[k].to_bits());
4795 }
4796 vel_row += 1;
4797 } else {
4798 coord_row += 1;
4799 }
4800 }
4801 assert_eq!(frame_i, 2);
4802 assert_eq!(vel_row, 5);
4803 }
4804}
4805
4806#[cfg(test)]
4807mod newline_skip_tests {
4808 use super::ConFrameIterator;
4809
4810 fn naive(bytes: &[u8], n: usize) -> Result<usize, ()> {
4813 let mut pos = 0usize;
4814 for i in 0..n {
4815 let rest = &bytes[pos..];
4816 match memchr::memchr(b'\n', rest) {
4817 Some(at) => pos += at + 1,
4818 None => {
4819 if rest.is_empty() {
4820 return Err(());
4821 }
4822 pos = bytes.len();
4823 if i + 1 == n {
4824 return Ok(pos);
4825 }
4826 return Err(());
4827 }
4828 }
4829 }
4830 Ok(pos)
4831 }
4832
4833 #[test]
4834 fn counted_skip_matches_per_line_memchr() {
4835 let mut samples = vec![
4836 String::new(),
4837 "a".into(),
4838 "a\n".into(),
4839 "a\nb".into(),
4840 "\n".into(),
4841 "\n\n".into(),
4842 "abc\ndef\nghi".into(),
4843 "a\r\nb\r\n".into(),
4844 "x".repeat(100) + "\n" + &"y".repeat(40),
4845 ];
4846 let path =
4847 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/cuh2.con");
4848 let one = std::fs::read_to_string(&path).unwrap();
4849 samples.push(one.repeat(2));
4850 let bare = one.trim_end_matches(['\n', '\r']).to_string();
4851 samples.push(bare);
4852 for text in samples {
4853 let bytes = text.as_bytes();
4854 for n in [0usize, 1, 2, 3, 6, 7, 9, 40, 200, 450, 1000, 5000] {
4855 let expect = naive(bytes, n);
4856 let mut it = ConFrameIterator::from_bytes(bytes);
4857 let got = it.advance_lines(n).map(|_| it.byte_pos()).map_err(|_| ());
4858 assert_eq!(got, expect, "n={n} len={}", bytes.len());
4859 }
4860 }
4861 }
4862}
4863
4864#[cfg(test)]
4865mod cached_unit_table_read_paths {
4866 use super::*;
4867
4868 const FRAME: &str = "\
4869v3
4870{\"con_spec_version\":3,\"con_spec_minor\":1,\"units\":{\"length\":\"angstrom\",\"energy\":\"eV\"}}
487110 10 10
487290 90 90
4873
4874
48751
48761
48771.0
4878H
4879Coordinates of Component 1
48800.5 0.25 0.125 0 7
4881";
4882
4883 fn coords(frame: &types::ConFrame) -> [f64; 3] {
4884 frame.positions.as_f64_row(0)
4885 }
4886
4887 #[test]
4890 fn read_paths_share_the_cached_unit_table() {
4891 let addr = crate::units::cached_unit_table_addr();
4892 let serial = ConFrameIterator::new(FRAME)
4893 .next()
4894 .expect("frame")
4895 .expect("parse");
4896 let from_bytes = frames_from_bytes(FRAME.as_bytes(), Some(1)).expect("bytes");
4897 let phased = frames_from_bytes_phased(FRAME.as_bytes(), Some(1)).expect("phased");
4898 assert_eq!(from_bytes.len(), 1);
4899 assert_eq!(phased.frames.len(), 1);
4900 assert_eq!(coords(&from_bytes[0]), coords(&serial));
4901 assert_eq!(coords(&phased.frames[0]), coords(&serial));
4902 assert_eq!(crate::units::cached_unit_table_addr(), addr);
4903 let bad = FRAME.replace("angstrom", "not-a-unit");
4904 assert!(ConFrameIterator::new(&bad).next().unwrap().is_err());
4905 assert!(frames_from_bytes(bad.as_bytes(), Some(1)).is_err());
4906 assert!(frames_from_bytes_phased(bad.as_bytes(), Some(1)).is_err());
4907 assert_eq!(crate::units::cached_unit_table_addr(), addr);
4908 }
4909}