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(
1073 text: &str,
1074 num_threads: Option<usize>,
1075) -> Result<Vec<types::ConFrame>, error::ParseError> {
1076 #[cfg(feature = "parallel")]
1077 {
1078 if let Some(n) = num_threads.filter(|n| *n >= 2) {
1079 let parts = parse_frames_parallel_with_threads(text, Some(n));
1080 let mut frames = Vec::with_capacity(parts.len());
1081 for r in parts {
1082 frames.push(r?);
1083 }
1084 return Ok(frames);
1085 }
1086 }
1087 #[cfg(not(feature = "parallel"))]
1088 {
1089 let _ = num_threads;
1090 }
1091 ConFrameIterator::new(text).collect()
1092}
1093
1094pub fn read_all_frames_with_threads(
1106 path: &Path,
1107 num_threads: Option<usize>,
1108) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
1109 let bytes = crate::compression::read_bytes_owned(path)?;
1110 Ok(frames_from_bytes_located(bytes.as_slice(), num_threads)?)
1111}
1112
1113pub struct PositionBlock {
1124 pub n_frames: usize,
1125 pub n_atoms: usize,
1126 pub values: Vec<f64>,
1127}
1128
1129pub fn read_positions_with_threads(
1130 path: &Path,
1131 num_threads: Option<usize>,
1132) -> Result<PositionBlock, Box<dyn std::error::Error>> {
1133 let contents = crate::compression::read_file_contents(path)?;
1134 let block = match &contents {
1135 crate::compression::FileContents::Owned(text) => {
1136 fill_position_block(text.as_bytes(), num_threads)
1137 }
1138 crate::compression::FileContents::Mapped(map) => fill_position_block(map, num_threads),
1139 }?;
1140 Ok(block)
1141}
1142
1143fn file_line_at_bytes(bytes: &[u8], byte: usize) -> u32 {
1145 let end = byte.min(bytes.len());
1146 1 + memchr::memchr_iter(b'\n', &bytes[..end]).count() as u32
1147}
1148
1149fn share_cursor(local: u32, bytes: &[u8], share_start: usize) -> u32 {
1154 let base = file_line_at_bytes(bytes, share_start);
1155 if local == 0 {
1156 base
1157 } else {
1158 base.saturating_add(local.saturating_sub(1))
1159 }
1160}
1161
1162fn frame_starts(bytes: &[u8]) -> Result<Vec<usize>, error::LocatedParseError> {
1163 let mut scanner = ConFrameIterator::from_bytes(bytes);
1164 let mut boundaries = Vec::new();
1165 loop {
1166 scanner.lines.clear_peek();
1167 let start = scanner.lines.pos;
1168 if start >= bytes.len() || bytes[start..].iter().all(u8::is_ascii_whitespace) {
1169 break;
1170 }
1171 boundaries.push(start);
1172 match scanner.forward_fast() {
1173 Some(Ok(())) => {}
1174 Some(Err(err)) => {
1175 return Err(error::LocatedParseError::new(
1176 err,
1177 scanner.lines.line_no(),
1178 scanner.frame_index(),
1179 ));
1180 }
1181 None => break,
1182 }
1183 }
1184 Ok(boundaries)
1185}
1186
1187fn parse_span_located(
1189 bytes: &[u8],
1190 start: usize,
1191 end: usize,
1192 frame_index: u64,
1193) -> Result<types::ConFrame, error::LocatedParseError> {
1194 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
1195 match iter.next() {
1196 Some(Ok(frame)) => Ok(frame),
1197 Some(Err(err)) => Err(error::LocatedParseError::new(
1198 err,
1199 share_cursor(iter.lines.line_no(), bytes, start),
1200 frame_index,
1201 )),
1202 None => Err(error::LocatedParseError::new(
1203 error::ParseError::IncompleteFrame,
1204 file_line_at_bytes(bytes, start),
1205 frame_index,
1206 )),
1207 }
1208}
1209
1210fn copy_positions(frame: &types::ConFrame, dest: &mut [f64]) -> Result<(), error::ParseError> {
1211 if frame.positions.nrows().saturating_mul(3) != dest.len() {
1212 return Err(error::ParseError::InvalidCount(
1213 "frame atom count does not match the first frame".into(),
1214 ));
1215 }
1216 if let Some(src) = frame.positions.f64_slice() {
1217 dest.copy_from_slice(src);
1218 return Ok(());
1219 }
1220 for (i, slot) in dest.chunks_exact_mut(3).enumerate() {
1221 slot.copy_from_slice(&frame.positions.as_f64_row(i));
1222 }
1223 Ok(())
1224}
1225
1226fn fill_position_block(
1227 bytes: &[u8],
1228 num_threads: Option<usize>,
1229) -> Result<PositionBlock, error::LocatedParseError> {
1230 let boundaries = frame_starts(bytes)?;
1231 if boundaries.is_empty() {
1232 return Ok(PositionBlock {
1233 n_frames: 0,
1234 n_atoms: 0,
1235 values: Vec::new(),
1236 });
1237 }
1238 let end0 = boundaries.get(1).copied().unwrap_or(bytes.len());
1239 let first = parse_span_located(bytes, boundaries[0], end0, 0)?;
1240 let n_atoms = first.positions.nrows();
1241 let n_frames = boundaries.len();
1242 let stride = n_atoms.saturating_mul(3);
1243 let mut values = vec![0.0_f64; n_frames.saturating_mul(stride)];
1244 if stride == 0 {
1245 return Ok(PositionBlock {
1246 n_frames,
1247 n_atoms,
1248 values,
1249 });
1250 }
1251 fill_position_spans(bytes, &boundaries, n_atoms, num_threads, &mut values)?;
1252 Ok(PositionBlock {
1253 n_frames,
1254 n_atoms,
1255 values,
1256 })
1257}
1258
1259fn fill_position_spans(
1260 bytes: &[u8],
1261 boundaries: &[usize],
1262 n_atoms: usize,
1263 num_threads: Option<usize>,
1264 values: &mut [f64],
1265) -> Result<(), error::LocatedParseError> {
1266 let stride = n_atoms * 3;
1267 let write_one = |i: usize, dest: &mut [f64]| {
1268 let start = boundaries[i];
1269 let end = boundaries.get(i + 1).copied().unwrap_or(bytes.len());
1270 let frame = parse_span_located(bytes, start, end, i as u64)?;
1271 copy_positions(&frame, dest).map_err(|error| {
1272 error::LocatedParseError::new(error, file_line_at_bytes(bytes, start), i as u64)
1273 })
1274 };
1275 #[cfg(feature = "parallel")]
1276 {
1277 let parallel = !matches!(num_threads, Some(n) if n <= 1);
1278 if parallel {
1279 use rayon::prelude::*;
1280 let mut run = || match values
1285 .par_chunks_mut(stride)
1286 .enumerate()
1287 .find_map_first(|(i, dest)| write_one(i, dest).err())
1288 {
1289 Some(err) => Err(err),
1290 None => Ok(()),
1291 };
1292 return match num_threads {
1293 Some(n) if n >= 2 => {
1294 let pool = rayon::ThreadPoolBuilder::new()
1295 .num_threads(n)
1296 .build()
1297 .map_err(|err| {
1298 error::LocatedParseError::new(
1299 error::ParseError::ValidationError(err.to_string()),
1300 0,
1301 0,
1302 )
1303 })?;
1304 pool.install(run)
1305 }
1306 _ => run(),
1307 };
1308 }
1309 }
1310 #[cfg(not(feature = "parallel"))]
1311 {
1312 let _ = num_threads;
1313 }
1314 for (i, dest) in values.chunks_mut(stride).enumerate() {
1315 write_one(i, dest)?;
1316 }
1317 Ok(())
1318}
1319
1320pub fn frames_from_bytes(
1322 bytes: &[u8],
1323 num_threads: Option<usize>,
1324) -> Result<Vec<types::ConFrame>, error::ParseError> {
1325 #[cfg(feature = "parallel")]
1326 {
1327 if let Some(n) = num_threads.filter(|n| *n >= 2) {
1328 let parts = parse_frames_parallel_bytes(bytes, Some(n));
1329 let mut frames = Vec::with_capacity(parts.len());
1330 for r in parts {
1331 frames.push(r?);
1332 }
1333 return Ok(frames);
1334 }
1335 }
1336 #[cfg(not(feature = "parallel"))]
1337 {
1338 let _ = num_threads;
1339 }
1340 let mut frames = Vec::new();
1341 for r in ConFrameIterator::from_bytes(bytes) {
1342 frames.push(r?);
1343 }
1344 Ok(frames)
1345}
1346
1347fn frames_from_bytes_located(
1349 bytes: &[u8],
1350 num_threads: Option<usize>,
1351) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
1352 #[cfg(feature = "parallel")]
1353 {
1354 if let Some(n) = num_threads.filter(|n| *n >= 2) {
1355 let timed = parse_bytes_timed(bytes, Some(n));
1356 return collect_located_slots(timed.slots, &timed.fail_lines);
1357 }
1358 }
1359 #[cfg(not(feature = "parallel"))]
1360 {
1361 let _ = num_threads;
1362 }
1363 collect_bytes_located(bytes)
1364}
1365
1366fn collect_bytes_located(bytes: &[u8]) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
1368 let mut frames = Vec::new();
1369 let mut iter = ConFrameIterator::from_bytes(bytes);
1370 while let Some(item) = iter.next() {
1371 match item {
1372 Ok(frame) => frames.push(frame),
1373 Err(error) => {
1374 return Err(error::LocatedParseError::new(
1375 error,
1376 iter.lines.line_no(),
1377 iter.frame_index(),
1378 ));
1379 }
1380 }
1381 }
1382 Ok(frames)
1383}
1384
1385#[cfg(feature = "parallel")]
1387fn collect_located_slots(
1388 slots: Vec<Result<types::ConFrame, error::ParseError>>,
1389 fail_lines: &[(usize, u32)],
1390) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
1391 let mut frames = Vec::with_capacity(slots.len());
1392 for (index, part) in slots.into_iter().enumerate() {
1393 match part {
1394 Ok(frame) => frames.push(frame),
1395 Err(error) => {
1396 let line = fail_lines
1397 .iter()
1398 .find(|(slot, _)| *slot == index)
1399 .map(|(_, line)| *line)
1400 .unwrap_or(0);
1401 return Err(error::LocatedParseError::new(error, line, index as u64));
1402 }
1403 }
1404 }
1405 Ok(frames)
1406}
1407
1408#[cfg(feature = "parallel")]
1409fn span_end(boundaries: &[usize], index: usize, len: usize) -> usize {
1410 if index + 1 < boundaries.len() {
1411 boundaries[index + 1]
1412 } else {
1413 len
1414 }
1415}
1416
1417#[cfg(feature = "parallel")]
1422#[derive(Copy, Clone)]
1423struct SlotWritePtr(*mut std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>);
1424
1425#[cfg(feature = "parallel")]
1426impl SlotWritePtr {
1427 unsafe fn write_slot(self, index: usize, value: Result<types::ConFrame, error::ParseError>) {
1430 unsafe {
1431 (*self.0.add(index)).write(value);
1432 }
1433 }
1434}
1435
1436#[cfg(feature = "parallel")]
1440unsafe impl Send for SlotWritePtr {}
1441
1442#[cfg(feature = "parallel")]
1445unsafe impl Sync for SlotWritePtr {}
1446
1447#[cfg(feature = "parallel")]
1452fn thread_minflt() -> i64 {
1453 #[cfg(target_os = "linux")]
1454 {
1455 unsafe {
1456 let mut usage = std::mem::zeroed::<libc::rusage>();
1457 libc::getrusage(libc::RUSAGE_THREAD, &mut usage);
1458 usage.ru_minflt
1459 }
1460 }
1461 #[cfg(not(target_os = "linux"))]
1462 {
1463 0
1464 }
1465}
1466
1467#[cfg(feature = "parallel")]
1473fn sample_llc_misses() -> i64 {
1474 #[cfg(not(target_os = "linux"))]
1476 {
1477 return -1;
1478 }
1479 #[cfg(target_os = "linux")]
1480 {
1481 #[repr(C)]
1482 struct PerfEventAttr {
1483 typ: u32,
1484 size: u32,
1485 config: u64,
1486 rest: [u64; 14],
1487 }
1488 let attr = PerfEventAttr {
1489 typ: 3,
1490 size: std::mem::size_of::<PerfEventAttr>() as u32,
1491 config: 2 | (1 << 16),
1493 rest: [0; 14],
1494 };
1495 let fd = unsafe {
1496 libc::syscall(
1497 libc::SYS_perf_event_open,
1498 &attr as *const PerfEventAttr,
1499 0i32,
1500 -1i32,
1501 -1i32,
1502 0u64,
1503 )
1504 };
1505 if fd < 0 {
1506 return -1;
1507 }
1508 let fd = fd as libc::c_int;
1509 let mut value = 0u64;
1510 let n = unsafe { libc::read(fd, &mut value as *mut u64 as *mut libc::c_void, 8) };
1511 unsafe {
1512 libc::close(fd);
1513 }
1514 if n == 8 { value as i64 } else { -1 }
1515 }
1516}
1517
1518#[cfg(feature = "parallel")]
1524fn numa_remote_pages() -> i64 {
1525 let online = std::fs::read_to_string("/sys/devices/system/node/online").unwrap_or_default();
1526 let online = online.trim();
1527 let nodes = if let Some((start, end)) = online.split_once('-') {
1528 let start: i64 = start.parse().unwrap_or(0);
1529 let end: i64 = end.parse().unwrap_or(0);
1530 end - start + 1
1531 } else {
1532 1
1533 };
1534 if nodes <= 1 {
1535 return 0;
1536 }
1537 let text = std::fs::read_to_string("/proc/self/numa_maps").unwrap_or_default();
1538 let mut remote = 0i64;
1539 for line in text.lines() {
1540 for token in line.split_whitespace() {
1541 if let Some(rest) = token.strip_prefix('N') {
1542 if let Some((node, pages)) = rest.split_once('=') {
1543 if node != "0" {
1544 remote += pages.parse::<i64>().unwrap_or(0);
1545 }
1546 }
1547 }
1548 }
1549 }
1550 remote
1551}
1552
1553#[cfg(feature = "parallel")]
1560fn byte_balanced_ranges(boundaries: &[usize], len: usize, workers: usize) -> Vec<(usize, usize)> {
1561 let n = boundaries.len();
1562 if n == 0 {
1563 return Vec::new();
1564 }
1565 let workers = workers.max(1).min(n);
1566 let chunks = workers.saturating_mul(4).clamp(workers, n);
1567 let origin = boundaries[0];
1568 let total = len.saturating_sub(origin).max(1) as u128;
1569 let mut ranges = Vec::with_capacity(chunks);
1570 let mut start = 0usize;
1571 for chunk in 0..chunks {
1572 if start >= n {
1573 break;
1574 }
1575 let end = if chunk + 1 == chunks {
1576 n
1577 } else {
1578 let target = origin + ((total * (chunk as u128 + 1)) / chunks as u128) as usize;
1579 let mut idx = if start + 1 < n {
1580 boundaries[start + 1..].partition_point(|&b| b < target) + start + 1
1581 } else {
1582 n
1583 };
1584 let min_end = start + 1;
1585 let reserved = chunks - chunk - 1;
1586 let max_end = n.saturating_sub(reserved);
1587 if max_end >= min_end {
1588 idx = idx.clamp(min_end, max_end);
1589 } else {
1590 idx = min_end.min(n);
1591 }
1592 idx
1593 };
1594 if end > start {
1595 ranges.push((start, end));
1596 start = end;
1597 }
1598 }
1599 if start < n {
1600 match ranges.last_mut() {
1601 Some(last) => last.1 = n,
1602 None => ranges.push((start, n)),
1603 }
1604 }
1605 ranges
1606}
1607
1608#[cfg(feature = "parallel")]
1615fn parse_into_slots<F>(
1616 bytes: &[u8],
1617 len: usize,
1618 boundaries: &[usize],
1619 workers: usize,
1620 parse_span: F,
1621) -> Vec<Result<types::ConFrame, error::ParseError>>
1622where
1623 F: Fn(usize, usize) -> Result<types::ConFrame, error::ParseError> + Sync,
1624{
1625 parse_into_slots_timed(bytes, len, boundaries, workers, parse_span).slots
1626}
1627
1628#[cfg(feature = "parallel")]
1630struct SlotParse {
1631 slots: Vec<Result<types::ConFrame, error::ParseError>>,
1632 alloc_s: f64,
1633 work_s: f64,
1634 sum_s: f64,
1635 max_s: f64,
1636 chunk_s: Vec<f64>,
1638 worker_minflt: Vec<i64>,
1640 share_frames: Vec<u64>,
1642 share_bytes: Vec<u64>,
1644 fail_lines: Vec<(usize, u32)>,
1647}
1648
1649#[cfg(feature = "parallel")]
1657fn parse_into_slots_timed<F>(
1658 bytes: &[u8],
1659 len: usize,
1660 boundaries: &[usize],
1661 workers: usize,
1662 parse_span: F,
1663) -> SlotParse
1664where
1665 F: Fn(usize, usize) -> Result<types::ConFrame, error::ParseError> + Sync,
1666{
1667 let n = boundaries.len();
1668 if n == 0 {
1669 return SlotParse {
1670 slots: Vec::new(),
1671 alloc_s: 0.0,
1672 work_s: 0.0,
1673 sum_s: 0.0,
1674 max_s: 0.0,
1675 chunk_s: Vec::new(),
1676 worker_minflt: Vec::new(),
1677 share_frames: Vec::new(),
1678 share_bytes: Vec::new(),
1679 fail_lines: Vec::new(),
1680 };
1681 }
1682 if workers < 2 {
1683 let t_alloc = std::time::Instant::now();
1684 let mut slots = Vec::with_capacity(n);
1685 let alloc_s = t_alloc.elapsed().as_secs_f64();
1686 let flt0 = thread_minflt();
1687 let t_work = std::time::Instant::now();
1688 for i in 0..n {
1689 slots.push(parse_span(boundaries[i], span_end(boundaries, i, len)));
1690 }
1691 let work_s = t_work.elapsed().as_secs_f64();
1692 let worker_minflt = vec![thread_minflt().saturating_sub(flt0)];
1693 return SlotParse {
1694 slots,
1695 alloc_s,
1696 work_s,
1697 sum_s: work_s,
1698 max_s: work_s,
1699 chunk_s: vec![work_s],
1700 worker_minflt,
1701 share_frames: vec![n as u64],
1702 share_bytes: vec![len as u64],
1703 fail_lines: Vec::new(),
1704 };
1705 }
1706 let t_alloc = std::time::Instant::now();
1707 let mut slots = take_slot_buf(n);
1708 let alloc_s = t_alloc.elapsed().as_secs_f64();
1709 let ranges = byte_balanced_ranges(boundaries, len, workers);
1713 let chunk_ns: Vec<std::sync::atomic::AtomicU64> = (0..ranges.len())
1714 .map(|_| std::sync::atomic::AtomicU64::new(0))
1715 .collect();
1716 let chunk_flt: Vec<std::sync::atomic::AtomicI64> = (0..ranges.len())
1717 .map(|_| std::sync::atomic::AtomicI64::new(0))
1718 .collect();
1719 let slots_ptr = SlotWritePtr(slots.as_mut_ptr());
1720 let fail_lines = std::sync::Mutex::new(Vec::<(usize, u32)>::new());
1721 let t_work = std::time::Instant::now();
1722 {
1723 let parse_range = |range_index: usize, start: usize, end: usize| {
1724 if start >= end {
1725 return;
1726 }
1727 let flt0 = thread_minflt();
1728 let t_chunk = std::time::Instant::now();
1729 let byte_start = boundaries[start];
1730 let byte_end = span_end(boundaries, end - 1, len);
1731 let mut iter = ConFrameIterator::from_bytes(&bytes[byte_start..byte_end]);
1732 for i in start..end {
1733 let frame = match iter.next() {
1734 Some(Ok(frame)) => Ok(frame),
1735 Some(Err(error)) => {
1736 let line = share_cursor(iter.lines.line_no(), bytes, byte_start);
1737 fail_lines.lock().expect("fail lines").push((i, line));
1738 Err(error)
1739 }
1740 None => {
1741 let line = share_cursor(iter.lines.line_no(), bytes, byte_start);
1742 fail_lines.lock().expect("fail lines").push((i, line));
1743 Err(error::ParseError::IncompleteFrame)
1744 }
1745 };
1746 unsafe {
1749 slots_ptr.write_slot(i, frame);
1750 }
1751 }
1752 chunk_ns[range_index].store(
1753 t_chunk.elapsed().as_nanos() as u64,
1754 std::sync::atomic::Ordering::Relaxed,
1755 );
1756 chunk_flt[range_index].store(
1757 thread_minflt().saturating_sub(flt0),
1758 std::sync::atomic::Ordering::Relaxed,
1759 );
1760 };
1761 let run_ranges = || {
1762 use rayon::prelude::*;
1763 ranges
1764 .par_iter()
1765 .enumerate()
1766 .for_each(|(range_index, &(start, end))| {
1767 parse_range(range_index, start, end);
1768 });
1769 };
1770 if rayon::current_thread_index().is_some() {
1771 run_ranges();
1772 } else {
1773 match rayon::ThreadPoolBuilder::new().num_threads(workers).build() {
1774 Ok(pool) => pool.install(run_ranges),
1775 Err(_) => {
1776 for (range_index, &(start, end)) in ranges.iter().enumerate() {
1777 parse_range(range_index, start, end);
1778 }
1779 }
1780 }
1781 }
1782 }
1783 let work_s = t_work.elapsed().as_secs_f64();
1784 let mut worker_sum_s = 0.0_f64;
1785 let mut worker_max_s = 0.0_f64;
1786 let mut chunk_s = Vec::new();
1787 let mut worker_minflt = Vec::new();
1788 let mut share_frames = Vec::new();
1789 let mut share_bytes = Vec::new();
1790 for (range_index, &(start, end)) in ranges.iter().enumerate() {
1791 if start >= end {
1792 continue;
1793 }
1794 let seconds =
1795 chunk_ns[range_index].load(std::sync::atomic::Ordering::Relaxed) as f64 / 1.0e9;
1796 chunk_s.push(seconds);
1797 worker_sum_s += seconds;
1798 worker_max_s = worker_max_s.max(seconds);
1799 worker_minflt.push(chunk_flt[range_index].load(std::sync::atomic::Ordering::Relaxed));
1800 share_frames.push((end - start) as u64);
1801 let nbytes = span_end(boundaries, end - 1, len).saturating_sub(boundaries[start]);
1802 share_bytes.push(nbytes as u64);
1803 }
1804 let slots = unsafe {
1807 let mut raw = std::mem::ManuallyDrop::new(slots);
1808 Vec::from_raw_parts(
1809 raw.as_mut_ptr() as *mut Result<types::ConFrame, error::ParseError>,
1810 n,
1811 raw.capacity(),
1812 )
1813 };
1814 let fail_lines = fail_lines.into_inner().unwrap_or_default();
1815 SlotParse {
1816 slots,
1817 alloc_s,
1818 work_s,
1819 sum_s: worker_sum_s,
1820 max_s: worker_max_s,
1821 chunk_s,
1822 worker_minflt,
1823 share_frames,
1824 share_bytes,
1825 fail_lines,
1826 }
1827}
1828
1829#[cfg(feature = "parallel")]
1830fn parse_byte_span(
1831 bytes: &[u8],
1832 start: usize,
1833 end: usize,
1834) -> Result<types::ConFrame, error::ParseError> {
1835 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
1836 match iter.next() {
1837 Some(result) => result,
1838 None => Err(error::ParseError::IncompleteFrame),
1839 }
1840}
1841
1842#[cfg(feature = "parallel")]
1843fn parse_text_span(
1844 text: &str,
1845 start: usize,
1846 end: usize,
1847) -> Result<types::ConFrame, error::ParseError> {
1848 let mut iter = ConFrameIterator::new(&text[start..end]);
1849 match iter.next() {
1850 Some(result) => result,
1851 None => Err(error::ParseError::IncompleteFrame),
1852 }
1853}
1854
1855#[cfg(feature = "parallel")]
1856struct TimedParse {
1857 slots: Vec<Result<types::ConFrame, error::ParseError>>,
1858 pool: Option<std::sync::Arc<rayon::ThreadPool>>,
1861 pool_s: f64,
1862 pool_join_s: f64,
1864 scan_s: f64,
1865 scan_minflt: i64,
1866 slot_alloc_s: f64,
1867 worker_parse_s: f64,
1868 worker_sum_s: f64,
1869 worker_max_s: f64,
1870 chunk_s: Vec<f64>,
1871 worker_minflt: Vec<i64>,
1872 share_frames: Vec<u64>,
1873 share_bytes: Vec<u64>,
1874 fail_lines: Vec<(usize, u32)>,
1876 llc_misses: i64,
1877 numa_remote: i64,
1878 scan_kind: u8,
1880}
1881
1882#[cfg(feature = "parallel")]
1888fn keep_parse_pages() {
1889 #[cfg(target_os = "linux")]
1893 unsafe {
1894 libc::mallopt(-4, 0);
1895 libc::mallopt(-1, i32::MAX);
1896 }
1897}
1898
1899#[cfg(feature = "parallel")]
1905fn take_slot_buf(
1906 n: usize,
1907) -> Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>> {
1908 let mut buf = slot_cache()
1909 .lock()
1910 .map(|mut slot| std::mem::take(&mut *slot))
1911 .unwrap_or_default();
1912 if buf.capacity() < n {
1913 buf = Vec::with_capacity(n);
1914 }
1915 unsafe { buf.set_len(n) };
1916 buf
1917}
1918
1919#[cfg(feature = "parallel")]
1920fn slot_cache()
1921-> &'static std::sync::Mutex<Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>>>
1922{
1923 static CACHE: std::sync::Mutex<
1924 Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>>,
1925 > = std::sync::Mutex::new(Vec::new());
1926 &CACHE
1927}
1928
1929#[cfg(feature = "parallel")]
1934#[allow(clippy::type_complexity)]
1935fn shared_pool(workers: usize) -> Option<std::sync::Arc<rayon::ThreadPool>> {
1936 use std::sync::{Arc, Mutex, OnceLock};
1937 static CACHE: OnceLock<Mutex<Vec<(usize, Arc<rayon::ThreadPool>)>>> = OnceLock::new();
1938 let cache = CACHE.get_or_init(|| Mutex::new(Vec::new()));
1939 let mut guard = cache.lock().ok()?;
1940 if let Some((_, pool)) = guard.iter().find(|(count, _)| *count == workers) {
1941 return Some(Arc::clone(pool));
1942 }
1943 keep_parse_pages();
1944 let pool = Arc::new(
1945 rayon::ThreadPoolBuilder::new()
1946 .num_threads(workers)
1947 .build()
1948 .ok()?,
1949 );
1950 guard.push((workers, Arc::clone(&pool)));
1951 Some(pool)
1952}
1953
1954#[cfg(feature = "parallel")]
1958pub fn process_minflt() -> i64 {
1959 #[cfg(unix)]
1960 {
1961 unsafe {
1962 let mut usage = std::mem::zeroed::<libc::rusage>();
1963 libc::getrusage(libc::RUSAGE_SELF, &mut usage);
1964 usage.ru_minflt
1965 }
1966 }
1967 #[cfg(not(unix))]
1968 {
1969 0
1970 }
1971}
1972
1973#[cfg(feature = "parallel")]
1975fn host_page_size() -> usize {
1976 #[cfg(unix)]
1977 {
1978 let raw = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
1979 if raw > 0 { raw as usize } else { 4096 }
1980 }
1981 #[cfg(not(unix))]
1982 {
1983 4096
1984 }
1985}
1986
1987#[cfg(feature = "parallel")]
1992fn first_touch_f64(values: &mut [f64], workers: usize) {
1993 use rayon::prelude::*;
1994 if values.is_empty() || workers < 2 {
1995 return;
1996 }
1997 let page = host_page_size().max(std::mem::size_of::<f64>());
1998 let step = (page / std::mem::size_of::<f64>()).max(1);
1999 let pages = values.len().div_ceil(step);
2000 let share = pages.div_ceil(workers);
2001 if rayon::current_thread_index().is_some() {
2002 values.par_chunks_mut(step).for_each(|chunk| {
2005 if let Some(slot) = chunk.first_mut() {
2006 *slot = 0.0;
2007 }
2008 });
2009 return;
2010 }
2011 std::thread::scope(|scope| {
2012 let mut rest = values;
2013 for worker in 0..workers {
2014 if rest.is_empty() {
2015 break;
2016 }
2017 let pages_here = share.min(rest.len().div_ceil(step));
2018 let take = (pages_here * step).min(rest.len());
2019 let (head, tail) = rest.split_at_mut(take);
2020 rest = tail;
2021 let _ = worker;
2022 scope.spawn(move || {
2023 let mut at = 0;
2024 while at < head.len() {
2025 head[at] = 0.0;
2026 at += step;
2027 }
2028 });
2029 }
2030 });
2031}
2032
2033#[cfg(feature = "parallel")]
2039fn finish_results(
2040 pool: Option<&rayon::ThreadPool>,
2041 slots: Vec<Result<types::ConFrame, error::ParseError>>,
2042) -> Result<Vec<types::ConFrame>, error::ParseError> {
2043 if slots.len() < 2 || pool.is_none() {
2044 let mut frames = Vec::with_capacity(slots.len());
2045 for part in slots {
2046 frames.push(part?);
2047 }
2048 return Ok(frames);
2049 }
2050 let pool = pool.expect("pool");
2051 pool.install(|| {
2052 use rayon::prelude::*;
2053 if let Some(index) = slots.par_iter().position_first(|item| item.is_err()) {
2054 return Err(match slots.into_iter().nth(index) {
2055 Some(Err(err)) => err,
2056 _ => error::ParseError::IncompleteFrame,
2057 });
2058 }
2059 Ok(slots
2060 .into_par_iter()
2061 .map(|item| item.expect("ok"))
2062 .collect())
2063 })
2064}
2065
2066#[cfg(feature = "parallel")]
2068pub const SCAN_SERIAL: u8 = 0;
2069#[cfg(feature = "parallel")]
2076pub const SCAN_INDEXED: u8 = 1;
2077#[cfg(feature = "parallel")]
2079pub const SCAN_EQUAL_SPAN: u8 = 2;
2080
2081#[cfg(feature = "parallel")]
2089#[cfg_attr(not(test), allow(dead_code))]
2090fn equal_span_boundaries(bytes: &[u8], workers: usize) -> Option<Vec<usize>> {
2091 if workers < 2 || bytes.len() < 2 {
2092 return None;
2093 }
2094 let mut first = ConFrameIterator::from_bytes(bytes);
2095 match first.forward_fast() {
2096 Some(Ok(())) => {}
2097 _ => return None,
2098 }
2099 let span = first.lines.pos;
2100 if span < 2 {
2101 return None;
2102 }
2103 let n = bytes.len() / span;
2104 if n < 2 {
2105 return None;
2106 }
2107 let tiled = n * span;
2110 if bytes[tiled..].iter().any(|b| !b.is_ascii_whitespace()) {
2111 return None;
2112 }
2113 let byte_share = tiled.div_ceil(workers);
2117 for worker in 0..workers {
2118 let byte_cut = worker.saturating_mul(byte_share).min(tiled);
2119 if byte_cut >= tiled {
2120 continue;
2121 }
2122 let index = if worker == 0 {
2123 0
2124 } else {
2125 byte_cut.div_ceil(span).min(n - 1)
2126 };
2127 let start = index.saturating_mul(span);
2128 if start >= tiled {
2129 continue;
2130 }
2131 let end = (start + span).min(tiled);
2132 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
2133 match iter.forward_fast() {
2134 Some(Ok(())) if iter.lines.pos == end - start => {}
2135 _ => return None,
2136 }
2137 }
2138 Some(arithmetic_starts(span, n))
2139}
2140
2141#[cfg(feature = "parallel")]
2146static START_CACHE: std::sync::Mutex<Vec<usize>> = std::sync::Mutex::new(Vec::new());
2147
2148#[cfg(feature = "parallel")]
2149#[cfg_attr(not(test), allow(dead_code))]
2150fn arithmetic_starts(span: usize, n: usize) -> Vec<usize> {
2151 let mut starts = START_CACHE
2152 .lock()
2153 .map(|mut slot| std::mem::take(&mut *slot))
2154 .unwrap_or_default();
2155 if starts.len() != n {
2156 starts.resize(n, 0);
2157 }
2158 for (index, slot) in starts.iter_mut().enumerate() {
2159 *slot = index * span;
2160 }
2161 starts
2162}
2163
2164#[cfg(feature = "parallel")]
2165fn recycle_starts(starts: Vec<usize>) {
2166 if let Ok(mut slot) = START_CACHE.lock() {
2167 if starts.capacity() >= slot.capacity() {
2168 *slot = starts;
2169 }
2170 }
2171}
2172
2173#[cfg(feature = "parallel")]
2174fn parse_frames_parallel_bytes(
2175 bytes: &[u8],
2176 num_threads: Option<usize>,
2177) -> Vec<Result<types::ConFrame, error::ParseError>> {
2178 parse_bytes_timed(bytes, num_threads).slots
2179}
2180
2181#[cfg(feature = "parallel")]
2188fn parse_bytes_timed(bytes: &[u8], num_threads: Option<usize>) -> TimedParse {
2189 let parse_workers = match num_threads {
2190 Some(n) if n >= 2 => n,
2191 _ => 1,
2192 };
2193 let run = || {
2194 let fault0 = process_minflt();
2195 let t_scan = std::time::Instant::now();
2196 let (boundaries, scan_kind) =
2200 if let Some(bounds) = equal_span_boundaries(bytes, parse_workers) {
2201 (bounds, SCAN_EQUAL_SPAN)
2202 } else {
2203 (
2204 collect_frame_starts(ConFrameIterator::from_bytes(bytes), true),
2205 SCAN_SERIAL,
2206 )
2207 };
2208 let scan_s = t_scan.elapsed().as_secs_f64();
2209 let scan_minflt = process_minflt().saturating_sub(fault0);
2210 let parsed = parse_into_slots_timed(
2211 bytes,
2212 bytes.len(),
2213 &boundaries,
2214 parse_workers,
2215 |start, end| parse_byte_span(bytes, start, end),
2216 );
2217 recycle_starts(boundaries);
2218 (parsed, scan_s, scan_kind, scan_minflt)
2219 };
2220 let (pool_s, pool, packed) = match num_threads {
2221 None => (0.0, None, run()),
2222 Some(n) if n <= 1 => {
2223 let _ = n;
2224 keep_parse_pages();
2225 let fault0 = process_minflt();
2226 let t_scan = std::time::Instant::now();
2227 let boundaries = collect_frame_starts(ConFrameIterator::from_bytes(bytes), true);
2228 let scan_s = t_scan.elapsed().as_secs_f64();
2229 let scan_minflt = process_minflt().saturating_sub(fault0);
2230 let parsed =
2231 parse_into_slots_timed(bytes, bytes.len(), &boundaries, 1, |start, end| {
2232 parse_byte_span(bytes, start, end)
2233 });
2234 (0.0, None, (parsed, scan_s, SCAN_SERIAL, scan_minflt))
2235 }
2236 Some(n) => {
2237 let t_pool = std::time::Instant::now();
2238 match shared_pool(n) {
2239 Some(pool) => {
2240 let built = t_pool.elapsed().as_secs_f64();
2241 let t_wake = std::time::Instant::now();
2242 let (wake, body) = pool.install(|| {
2243 let wake = t_wake.elapsed().as_secs_f64();
2244 (wake, run())
2245 });
2246 (built + wake, Some(pool), body)
2249 }
2250 None => (t_pool.elapsed().as_secs_f64(), None, run()),
2251 }
2252 }
2253 };
2254 let (parsed, scan_s, scan_kind, scan_minflt) = packed;
2255 TimedParse {
2256 slots: parsed.slots,
2257 pool,
2258 pool_s,
2259 pool_join_s: 0.0,
2260 scan_s,
2261 scan_minflt,
2262 slot_alloc_s: parsed.alloc_s,
2263 worker_parse_s: parsed.work_s,
2264 worker_sum_s: parsed.sum_s,
2265 worker_max_s: parsed.max_s,
2266 chunk_s: parsed.chunk_s,
2267 worker_minflt: parsed.worker_minflt,
2268 share_frames: parsed.share_frames,
2269 share_bytes: parsed.share_bytes,
2270 fail_lines: parsed.fail_lines,
2271 llc_misses: sample_llc_misses(),
2272 numa_remote: numa_remote_pages(),
2273 scan_kind,
2274 }
2275}
2276
2277pub fn count_frames(path: &Path) -> Result<usize, LoadError> {
2284 let contents = load_text(path)?;
2285 let text = contents.as_str().map_err(LoadError::Utf8)?;
2286 let mut n = 0usize;
2287 let mut iter = ConFrameIterator::new(text);
2288 loop {
2289 match iter.forward_fast() {
2290 Some(Ok(())) => n += 1,
2291 Some(Err(e)) => {
2292 return Err(LoadError::Parse(error::LocatedParseError::new(
2293 e,
2294 iter.lines.line_no(),
2295 iter.frame_index(),
2296 )));
2297 }
2298 None => break,
2299 }
2300 }
2301 Ok(n)
2302}
2303
2304pub fn read_first_frame(path: &Path) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
2309 let contents = crate::compression::read_file_contents(path)?;
2310 let text = contents.as_str()?;
2311 let mut iter = ConFrameIterator::new(text);
2312 match iter.next() {
2313 Some(Ok(frame)) => Ok(frame),
2314 Some(Err(e)) => Err(Box::new(e)),
2315 None => Err("No frames found in file".into()),
2316 }
2317}
2318
2319pub fn read_nth_frame(
2323 path: &Path,
2324 index: usize,
2325) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
2326 let contents = crate::compression::read_file_contents(path)?;
2327 let text = contents.as_str()?;
2328 read_nth_frame_from_text(text, index)
2329}
2330
2331pub fn read_nth_frame_from_text(
2333 text: &str,
2334 index: usize,
2335) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
2336 let mut iter = ConFrameIterator::new(text);
2337 let skipped = iter.skip_frames(index)?;
2338 if skipped < index {
2339 return Err(format!("frame {index} out of range (only {skipped} frames)").into());
2340 }
2341 match iter.next() {
2342 Some(Ok(frame)) => Ok(frame),
2343 Some(Err(e)) => Err(Box::new(e)),
2344 None => Err(format!("frame {index} out of range").into()),
2345 }
2346}
2347
2348#[cfg(feature = "parallel")]
2354pub fn read_all_frames_indexed(
2355 path: &Path,
2356 num_threads: Option<usize>,
2357) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
2358 let contents = crate::compression::read_file_contents(path)?;
2359 let text = contents.as_str_sliced()?;
2360 let idx = match contents {
2361 crate::compression::FileContents::Mapped(_) => {
2362 crate::frame_index::load_or_build(path, text, true)?
2363 }
2364 crate::compression::FileContents::Owned(_) => {
2365 let on_disk = std::fs::metadata(path)?.len();
2366 crate::frame_index::load_or_build(path, text, on_disk == text.len() as u64)?
2367 }
2368 };
2369 let offs: Vec<usize> = idx.offsets.iter().map(|&o| o as usize).collect();
2370 parse_frames_at_offsets(text, &offs, num_threads)
2371 .into_iter()
2372 .map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error>))
2373 .collect()
2374}
2375
2376enum IndexStep {
2377 Stay(usize),
2378 Missing(usize),
2379 Landed(usize, usize),
2380 Short(usize, usize),
2381}
2382
2383struct IndexedSpan {
2384 start: usize,
2385 content_end: usize,
2386 next_pos: usize,
2387}
2388
2389fn line_count(nl: &[usize], len: usize) -> usize {
2391 if len == 0 {
2392 0
2393 } else if nl.last().copied() == Some(len - 1) {
2394 nl.len()
2395 } else {
2396 nl.len() + 1
2397 }
2398}
2399
2400fn line_start_pos(nl: &[usize], len: usize, line_no: usize) -> usize {
2401 if line_no == 0 {
2402 return 0;
2403 }
2404 if line_no >= line_count(nl, len) {
2405 return len;
2406 }
2407 nl[line_no - 1] + 1
2408}
2409
2410fn line_no_for_pos(nl: &[usize], len: usize, pos: usize) -> usize {
2412 if pos >= len {
2413 return line_count(nl, len);
2414 }
2415 nl.partition_point(|&p| p < pos)
2416}
2417
2418fn indexed_span(nl: &[usize], bytes: &[u8], line_no: usize) -> Option<IndexedSpan> {
2419 let len = bytes.len();
2420 if line_no >= line_count(nl, len) {
2421 return None;
2422 }
2423 let start = if line_no == 0 { 0 } else { nl[line_no - 1] + 1 };
2424 let (raw_end, next_pos) = if line_no < nl.len() {
2425 (nl[line_no], nl[line_no] + 1)
2426 } else {
2427 (len, len)
2428 };
2429 let mut content_end = raw_end;
2430 if content_end > start && bytes[content_end - 1] == b'\r' {
2431 content_end -= 1;
2432 }
2433 Some(IndexedSpan {
2434 start,
2435 content_end,
2436 next_pos,
2437 })
2438}
2439
2440#[cfg(feature = "parallel")]
2442fn equal_cuts(len: usize, n: usize) -> Vec<usize> {
2443 let n = n.max(1);
2444 let mut cuts = Vec::with_capacity(n + 1);
2445 for i in 0..=n {
2446 let at = ((i as u128) * (len as u128) / (n as u128)) as usize;
2447 let at = at.min(len);
2448 if cuts.last().copied() != Some(at) {
2449 cuts.push(at);
2450 }
2451 }
2452 if cuts.last().copied() != Some(len) {
2453 cuts.push(len);
2454 }
2455 cuts
2456}
2457
2458#[cfg(feature = "parallel")]
2465fn newline_positions_at_cuts(bytes: &[u8], cuts: &[usize]) -> Vec<usize> {
2466 let spans: Vec<(usize, usize)> = cuts
2467 .windows(2)
2468 .map(|w| (w[0].min(bytes.len()), w[1].min(bytes.len())))
2469 .filter(|(start, end)| start < end)
2470 .collect();
2471 if spans.len() <= 1 {
2472 let (start, end) = spans.first().copied().unwrap_or((0, 0));
2473 return memchr::memchr_iter(b'\n', &bytes[start..end])
2474 .map(|p| start + p)
2475 .collect();
2476 }
2477 use rayon::prelude::*;
2481 let parts: Vec<Vec<usize>> = spans
2482 .par_iter()
2483 .map(|&(start, end)| {
2484 memchr::memchr_iter(b'\n', &bytes[start..end])
2485 .map(|p| start + p)
2486 .collect()
2487 })
2488 .collect();
2489 let mut all = Vec::with_capacity(bytes.len() / 32);
2490 for part in parts {
2491 all.extend(part);
2492 }
2493 all
2494}
2495
2496#[cfg(feature = "parallel")]
2497fn newline_positions(bytes: &[u8], threads: usize) -> Vec<usize> {
2498 newline_positions_at_cuts(bytes, &equal_cuts(bytes.len(), threads))
2499}
2500
2501#[cfg(feature = "parallel")]
2511fn scan_thread_count(num_threads: Option<usize>, len: usize) -> usize {
2512 let requested = match num_threads {
2513 Some(n) if n >= 2 => n,
2514 _ => return 1,
2515 };
2516 if len < 2 { 1 } else { requested }
2517}
2518
2519#[derive(Debug)]
2521pub enum LoadError {
2522 Io(std::io::Error),
2523 Utf8(std::str::Utf8Error),
2524 Parse(error::LocatedParseError),
2525 Empty,
2527}
2528
2529impl std::fmt::Display for LoadError {
2530 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2531 match self {
2532 LoadError::Io(e) => write!(f, "{e}"),
2533 LoadError::Utf8(e) => write!(f, "{e}"),
2534 LoadError::Parse(e) => write!(f, "{e}"),
2535 LoadError::Empty => write!(f, "no frames found in file"),
2536 }
2537 }
2538}
2539
2540impl std::error::Error for LoadError {}
2541
2542fn load_error_from_box(err: Box<dyn std::error::Error>) -> LoadError {
2543 if let Some(io) = err.downcast_ref::<std::io::Error>() {
2544 return LoadError::Io(std::io::Error::new(io.kind(), io.to_string()));
2545 }
2546 LoadError::Io(std::io::Error::other(err.to_string()))
2547}
2548
2549fn load_text(path: &Path) -> Result<crate::compression::FileContents, LoadError> {
2550 crate::compression::read_file_contents(path).map_err(load_error_from_box)
2551}
2552
2553#[cfg(feature = "parallel")]
2555fn file_line_at(text: &str, byte: usize) -> u32 {
2556 let end = byte.min(text.len());
2557 1 + text.as_bytes()[..end]
2558 .iter()
2559 .filter(|&&b| b == b'\n')
2560 .count() as u32
2561}
2562
2563pub fn frames_from_text_located(
2565 text: &str,
2566 num_threads: Option<usize>,
2567) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
2568 #[cfg(feature = "parallel")]
2569 {
2570 let use_parallel = matches!(num_threads, Some(n) if n >= 2);
2573 if use_parallel {
2574 let pool = num_threads.filter(|&n| n >= 2);
2575 return collect_parallel_located(text, pool);
2576 }
2577 }
2578 #[cfg(not(feature = "parallel"))]
2579 {
2580 let _ = num_threads;
2581 }
2582 let mut frames = Vec::new();
2583 let mut iter = ConFrameIterator::new(text);
2584 let mut index = 0u64;
2585 while let Some(item) = iter.next() {
2586 match item {
2587 Ok(frame) => {
2588 frames.push(frame);
2589 index += 1;
2590 }
2591 Err(e) => {
2592 return Err(error::LocatedParseError::new(
2593 e,
2594 iter.lines.line_no(),
2595 index,
2596 ));
2597 }
2598 }
2599 }
2600 Ok(frames)
2601}
2602
2603pub fn read_all_frames_located(
2605 path: &Path,
2606 num_threads: Option<usize>,
2607) -> Result<Vec<types::ConFrame>, LoadError> {
2608 let contents = load_text(path)?;
2609 let text = contents.as_str().map_err(LoadError::Utf8)?;
2610 frames_from_text_located(text, num_threads).map_err(LoadError::Parse)
2611}
2612
2613pub fn read_first_frame_located(path: &Path) -> Result<types::ConFrame, LoadError> {
2615 let contents = load_text(path)?;
2616 let text = contents.as_str().map_err(LoadError::Utf8)?;
2617 let mut iter = ConFrameIterator::new(text);
2618 match iter.next() {
2619 Some(Ok(frame)) => Ok(frame),
2620 Some(Err(e)) => Err(LoadError::Parse(error::LocatedParseError::new(
2621 e,
2622 iter.lines.line_no(),
2623 0,
2624 ))),
2625 None => Err(LoadError::Empty),
2626 }
2627}
2628
2629pub fn read_nth_frame_located(path: &Path, index: usize) -> Result<types::ConFrame, LoadError> {
2631 let contents = load_text(path)?;
2632 let text = contents.as_str().map_err(LoadError::Utf8)?;
2633 let mut iter = ConFrameIterator::new(text);
2634 match iter.skip_frames_counted(index) {
2635 Ok(skipped) if skipped == index => {}
2636 Ok(skipped) => {
2637 return Err(LoadError::Parse(error::LocatedParseError::new(
2638 error::ParseError::IncompleteFrame,
2639 iter.lines.line_no(),
2640 skipped as u64,
2641 )));
2642 }
2643 Err((failed_at, e)) => {
2644 return Err(LoadError::Parse(error::LocatedParseError::new(
2645 e,
2646 iter.lines.line_no(),
2647 failed_at as u64,
2648 )));
2649 }
2650 }
2651 match iter.next() {
2652 Some(Ok(frame)) => Ok(frame),
2653 Some(Err(e)) => Err(LoadError::Parse(error::LocatedParseError::new(
2654 e,
2655 iter.lines.line_no(),
2656 index as u64,
2657 ))),
2658 None => Err(LoadError::Parse(error::LocatedParseError::new(
2659 error::ParseError::IncompleteFrame,
2660 iter.lines.line_no(),
2661 index as u64,
2662 ))),
2663 }
2664}
2665
2666fn collect_frame_starts(scanner: ConFrameIterator<'_>, skip_whitespace_tail: bool) -> Vec<usize> {
2667 #[cfg(target_arch = "x86_64")]
2668 {
2669 if std::is_x86_feature_detected!("avx2") {
2670 return unsafe { collect_frame_starts_avx2(scanner, skip_whitespace_tail) };
2674 }
2675 }
2676 collect_frame_starts_inner(scanner, skip_whitespace_tail)
2677}
2678
2679#[cfg(target_arch = "x86_64")]
2680#[target_feature(enable = "avx2,popcnt")]
2681unsafe fn collect_frame_starts_avx2(
2682 scanner: ConFrameIterator<'_>,
2683 skip_whitespace_tail: bool,
2684) -> Vec<usize> {
2685 collect_frame_starts_inner(scanner, skip_whitespace_tail)
2686}
2687
2688#[inline(always)]
2689fn collect_frame_starts_inner(
2690 mut scanner: ConFrameIterator<'_>,
2691 skip_whitespace_tail: bool,
2692) -> Vec<usize> {
2693 let mut boundaries = Vec::new();
2694 loop {
2695 scanner.lines.clear_peek();
2696 let start = scanner.lines.pos;
2697 if start >= scanner.lines.bytes.len() {
2698 break;
2699 }
2700 if skip_whitespace_tail {
2701 let rest = &scanner.lines.bytes[start..];
2702 if rest.iter().all(u8::is_ascii_whitespace) {
2703 break;
2704 }
2705 }
2706 boundaries.push(start);
2707 match scanner.forward_fast_to(None) {
2708 Some(Ok(())) => {}
2709 Some(Err(_)) | None => break,
2710 }
2711 }
2712 boundaries
2713}
2714
2715pub fn frame_start_offsets_serial(file_contents: &str) -> Vec<usize> {
2717 collect_frame_starts(ConFrameIterator::new(file_contents), false)
2718}
2719
2720#[cfg(feature = "parallel")]
2721fn scanner_with_newlines<'a>(text: &'a str, newlines: Vec<usize>) -> ConFrameIterator<'a> {
2722 let mut scanner = ConFrameIterator::new(text);
2723 scanner.lines.index_line = 0;
2724 scanner.lines.newlines = Some(newlines);
2725 scanner
2726}
2727
2728#[cfg(feature = "parallel")]
2729fn frame_start_offsets_indexed(text: &str, newlines: Vec<usize>) -> Vec<usize> {
2730 collect_frame_starts(scanner_with_newlines(text, newlines), false)
2731}
2732
2733#[cfg(all(feature = "parallel", test))]
2739fn frame_start_offsets_at_cuts(text: &str, cuts: &[usize]) -> Vec<usize> {
2740 let newlines = newline_positions_at_cuts(text.as_bytes(), cuts);
2741 frame_start_offsets_indexed(text, newlines)
2742}
2743
2744pub fn frame_start_offsets(file_contents: &str) -> Vec<usize> {
2756 frame_start_offsets_with_threads(file_contents, None)
2757}
2758
2759pub fn frame_start_offsets_with_threads(
2767 file_contents: &str,
2768 num_threads: Option<usize>,
2769) -> Vec<usize> {
2770 #[cfg(feature = "parallel")]
2771 {
2772 let threads = scan_thread_count(num_threads, file_contents.len());
2773 if threads >= 2 {
2774 let newlines = newline_positions(file_contents.as_bytes(), threads);
2775 return frame_start_offsets_indexed(file_contents, newlines);
2776 }
2777 }
2778 #[cfg(not(feature = "parallel"))]
2779 {
2780 let _ = num_threads;
2781 }
2782 frame_start_offsets_serial(file_contents)
2783}
2784
2785#[cfg(feature = "parallel")]
2794pub fn parse_frames_parallel(
2795 file_contents: &str,
2796) -> Vec<Result<types::ConFrame, error::ParseError>> {
2797 parse_frames_parallel_with_threads(file_contents, None)
2798}
2799
2800#[cfg(feature = "parallel")]
2810pub fn parse_frames_parallel_with_threads(
2811 file_contents: &str,
2812 num_threads: Option<usize>,
2813) -> Vec<Result<types::ConFrame, error::ParseError>> {
2814 let parse_workers = match num_threads {
2815 Some(n) if n >= 2 => n,
2816 _ => 1,
2817 };
2818 let run = || {
2819 let boundaries = collect_frame_starts(ConFrameIterator::new(file_contents), true);
2823 parse_into_slots(
2824 file_contents.as_bytes(),
2825 file_contents.len(),
2826 &boundaries,
2827 parse_workers,
2828 |start, end| parse_text_span(file_contents, start, end),
2829 )
2830 };
2831 match num_threads {
2832 Some(n) if n >= 2 => match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
2833 Ok(pool) => pool.install(run),
2834 Err(_) => run(),
2835 },
2836 _ => run(),
2837 }
2838}
2839
2840#[cfg(feature = "parallel")]
2855pub fn parse_frames_at_offsets(
2856 file_contents: &str,
2857 boundaries: &[usize],
2858 num_threads: Option<usize>,
2859) -> Vec<Result<types::ConFrame, error::ParseError>> {
2860 let workers = match num_threads {
2861 Some(n) if n <= 1 => 1,
2862 Some(n) => n,
2863 None => rayon::current_num_threads().max(1),
2864 };
2865 let run = || {
2866 parse_into_slots(
2867 file_contents.as_bytes(),
2868 file_contents.len(),
2869 boundaries,
2870 workers,
2871 |start, end| parse_text_span(file_contents, start, end),
2872 )
2873 };
2874 match num_threads {
2875 Some(n) if n >= 2 => match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
2876 Ok(pool) => pool.install(run),
2877 Err(_) => run(),
2878 },
2879 _ => run(),
2880 }
2881}
2882
2883#[derive(Clone, Copy, Debug, Default)]
2886pub struct ReadPhaseTimes {
2887 pub open_mmap_s: f64,
2888 pub pool_s: f64,
2890 pub pool_join_s: f64,
2893 pub scan_s: f64,
2894 pub slot_alloc_s: f64,
2895 pub worker_parse_s: f64,
2896 pub finish_s: f64,
2898 pub worker_sum_s: f64,
2900 pub worker_max_s: f64,
2902 pub close_s: f64,
2904 pub schedule_s: f64,
2906 pub scan_minflt: i64,
2908 pub parse_minflt: i64,
2910 pub llc_misses: i64,
2912 pub numa_remote: i64,
2914 pub slab_minflt: i64,
2916 pub touch_minflt: i64,
2918 pub mapped: bool,
2920}
2921
2922pub struct PhasedFrames {
2924 pub frames: Vec<types::ConFrame>,
2925 pub phases: ReadPhaseTimes,
2926 pub n_bytes: usize,
2927 pub chunk_s: Vec<f64>,
2929 pub scan_kind: u8,
2932 pub worker_minflt: Vec<i64>,
2934 pub share_frames: Vec<u64>,
2936 pub share_bytes: Vec<u64>,
2938 #[cfg(feature = "parallel")]
2940 pub pool: Option<std::sync::Arc<rayon::ThreadPool>>,
2941}
2942
2943pub fn read_all_frames_phased(
2953 path: &Path,
2954 num_threads: Option<usize>,
2955) -> Result<PhasedFrames, Box<dyn std::error::Error>> {
2956 let t_all = std::time::Instant::now();
2957 let t_open = std::time::Instant::now();
2958 let contents = crate::compression::read_file_contents(path)?;
2959 let open_mmap_s = t_open.elapsed().as_secs_f64();
2960 let mapped = matches!(contents, crate::compression::FileContents::Mapped(_));
2961 let n_bytes = match &contents {
2962 crate::compression::FileContents::Owned(text) => text.len(),
2963 crate::compression::FileContents::Mapped(map) => map.len(),
2964 };
2965 let bytes: &[u8] = match &contents {
2966 crate::compression::FileContents::Owned(text) => text.as_bytes(),
2967 crate::compression::FileContents::Mapped(map) => map,
2968 };
2969 let phased = frames_from_bytes_phased_located(bytes, num_threads)?;
2970 let workers = match num_threads {
2971 Some(n) if n >= 2 => n,
2972 _ => 1,
2973 };
2974 let t_close = std::time::Instant::now();
2975 if let crate::compression::FileContents::Mapped(map) = &contents {
2976 if workers >= 2 {
2981 #[cfg(feature = "parallel")]
2982 {
2983 if let Some(pool) = phased.pool.as_ref() {
2984 pool.install(|| {
2985 crate::compression::advise_dontneed_parallel(map, workers);
2986 });
2987 }
2988 }
2989 #[cfg(not(feature = "parallel"))]
2990 {
2991 let _ = (map, workers);
2992 }
2993 }
2994 }
2995 drop(contents);
2996 let close_s = t_close.elapsed().as_secs_f64();
2997 let elapsed = t_all.elapsed().as_secs_f64();
2998 let named = open_mmap_s
2999 + phased.pool_s
3000 + phased.pool_join_s
3001 + phased.scan_s
3002 + phased.slot_alloc_s
3003 + phased.worker_parse_s
3004 + phased.finish_s
3005 + close_s;
3006 let schedule_s = (elapsed - named).max(0.0);
3007 let parse_minflt = phased.worker_minflt.iter().sum();
3008 Ok(PhasedFrames {
3009 frames: phased.frames,
3010 n_bytes,
3011 chunk_s: phased.chunk_s,
3012 scan_kind: phased.scan_kind,
3013 worker_minflt: phased.worker_minflt,
3014 share_frames: phased.share_frames,
3015 share_bytes: phased.share_bytes,
3016 phases: ReadPhaseTimes {
3017 open_mmap_s,
3018 pool_s: phased.pool_s,
3019 pool_join_s: phased.pool_join_s,
3020 scan_s: phased.scan_s,
3021 slot_alloc_s: phased.slot_alloc_s,
3022 worker_parse_s: phased.worker_parse_s,
3023 finish_s: phased.finish_s,
3024 worker_sum_s: phased.worker_sum_s,
3025 worker_max_s: phased.worker_max_s,
3026 close_s,
3027 schedule_s,
3028 scan_minflt: phased.scan_minflt,
3029 parse_minflt,
3030 llc_misses: phased.llc_misses,
3031 numa_remote: phased.numa_remote,
3032 mapped,
3033 ..ReadPhaseTimes::default()
3034 },
3035 #[cfg(feature = "parallel")]
3036 pool: phased.pool,
3037 })
3038}
3039
3040pub struct PhasedByteTimes {
3046 pub frames: Vec<types::ConFrame>,
3047 pub pool_s: f64,
3048 pub pool_join_s: f64,
3049 pub scan_s: f64,
3050 pub slot_alloc_s: f64,
3051 pub worker_parse_s: f64,
3052 pub finish_s: f64,
3053 pub worker_sum_s: f64,
3054 pub worker_max_s: f64,
3055 pub chunk_s: Vec<f64>,
3057 pub scan_kind: u8,
3060 pub scan_minflt: i64,
3062 pub worker_minflt: Vec<i64>,
3064 pub share_frames: Vec<u64>,
3065 pub share_bytes: Vec<u64>,
3066 pub llc_misses: i64,
3068 pub numa_remote: i64,
3070 #[cfg(feature = "parallel")]
3072 pub pool: Option<std::sync::Arc<rayon::ThreadPool>>,
3073}
3074
3075pub fn frames_from_bytes_phased(
3076 bytes: &[u8],
3077 num_threads: Option<usize>,
3078) -> Result<PhasedByteTimes, error::ParseError> {
3079 frames_from_bytes_phased_located(bytes, num_threads).map_err(|err| err.error)
3080}
3081
3082fn frames_from_bytes_phased_located(
3083 bytes: &[u8],
3084 num_threads: Option<usize>,
3085) -> Result<PhasedByteTimes, error::LocatedParseError> {
3086 #[cfg(feature = "parallel")]
3087 {
3088 if !matches!(num_threads, Some(n) if n >= 2) {
3091 let flt0 = thread_minflt();
3092 let t_work = std::time::Instant::now();
3093 let frames = collect_bytes_located(bytes)?;
3094 let worker_parse_s = t_work.elapsed().as_secs_f64();
3095 let worker_flt = thread_minflt().saturating_sub(flt0);
3096 let n_frames = frames.len() as u64;
3097 return Ok(PhasedByteTimes {
3098 frames,
3099 pool_s: 0.0,
3100 pool_join_s: 0.0,
3101 scan_s: 0.0,
3102 slot_alloc_s: 0.0,
3103 worker_parse_s,
3104 finish_s: 0.0,
3105 worker_sum_s: worker_parse_s,
3106 worker_max_s: worker_parse_s,
3107 chunk_s: vec![worker_parse_s],
3108 scan_kind: SCAN_SERIAL,
3109 scan_minflt: 0,
3110 worker_minflt: vec![worker_flt],
3111 share_frames: vec![n_frames],
3112 share_bytes: vec![bytes.len() as u64],
3113 llc_misses: sample_llc_misses(),
3114 numa_remote: numa_remote_pages(),
3115 pool: None,
3116 });
3117 }
3118 let requested = Some(num_threads.expect("workers"));
3119 let timed = parse_bytes_timed(bytes, requested);
3120 let t_finish = std::time::Instant::now();
3121 let frames = if timed.slots.iter().any(|item| item.is_err()) {
3122 collect_located_slots(timed.slots, &timed.fail_lines)?
3123 } else {
3124 finish_results(timed.pool.as_deref(), timed.slots)
3125 .map_err(|error| error::LocatedParseError::new(error, 0, 0))?
3126 };
3127 let finish_s = t_finish.elapsed().as_secs_f64();
3128 Ok(PhasedByteTimes {
3129 frames,
3130 pool_s: timed.pool_s,
3131 pool_join_s: timed.pool_join_s,
3132 scan_s: timed.scan_s,
3133 slot_alloc_s: timed.slot_alloc_s,
3134 worker_parse_s: timed.worker_parse_s,
3135 finish_s,
3136 worker_sum_s: timed.worker_sum_s,
3137 worker_max_s: timed.worker_max_s,
3138 chunk_s: timed.chunk_s,
3139 scan_kind: timed.scan_kind,
3140 scan_minflt: timed.scan_minflt,
3141 worker_minflt: timed.worker_minflt,
3142 share_frames: timed.share_frames,
3143 share_bytes: timed.share_bytes,
3144 llc_misses: timed.llc_misses,
3145 numa_remote: timed.numa_remote,
3146 pool: timed.pool,
3147 })
3148 }
3149 #[cfg(not(feature = "parallel"))]
3150 {
3151 let _ = num_threads;
3152 let t_work = std::time::Instant::now();
3153 let frames = collect_bytes_located(bytes)?;
3154 let worker_parse_s = t_work.elapsed().as_secs_f64();
3155 Ok(PhasedByteTimes {
3156 frames,
3157 pool_s: 0.0,
3158 pool_join_s: 0.0,
3159 scan_s: 0.0,
3160 slot_alloc_s: 0.0,
3161 worker_parse_s,
3162 finish_s: 0.0,
3163 worker_sum_s: worker_parse_s,
3164 worker_max_s: worker_parse_s,
3165 chunk_s: vec![worker_parse_s],
3166 scan_kind: 0,
3167 scan_minflt: 0,
3168 worker_minflt: Vec::new(),
3169 share_frames: Vec::new(),
3170 share_bytes: Vec::new(),
3171 llc_misses: -1,
3172 numa_remote: 0,
3173 })
3174 }
3175}
3176
3177pub fn pack_position_slab(frames: &[types::ConFrame], workers: usize) -> Option<Vec<f64>> {
3183 if frames.is_empty() {
3184 return None;
3185 }
3186 let n_atoms = frames[0].positions.nrows();
3187 if frames
3188 .iter()
3189 .any(|frame| frame.positions.nrows() != n_atoms)
3190 {
3191 return None;
3192 }
3193 let stride = n_atoms.saturating_mul(3);
3194 let total = frames.len().saturating_mul(stride);
3195 let mut values = Vec::<f64>::with_capacity(total);
3196 if total > 0 {
3197 unsafe {
3201 values.set_len(total);
3202 }
3203 }
3204 if stride == 0 {
3205 return Some(values);
3206 }
3207 let write = |frame: &types::ConFrame, dest: &mut [f64]| {
3208 if let Some(src) = frame.positions.f64_slice() {
3209 dest.copy_from_slice(src);
3210 } else {
3211 for (row, slot) in dest.chunks_exact_mut(3).enumerate() {
3212 slot.copy_from_slice(&frame.positions.as_f64_row(row));
3213 }
3214 }
3215 };
3216 #[cfg(feature = "parallel")]
3217 {
3218 if workers >= 2 {
3219 let chunk_frames = frames.len().div_ceil(workers).max(1);
3220 let chunk_vals = chunk_frames.saturating_mul(stride);
3221 let copy_chunks = |dest_all: &mut [f64]| {
3222 use rayon::prelude::*;
3223 dest_all
3224 .par_chunks_mut(chunk_vals)
3225 .enumerate()
3226 .for_each(|(chunk_index, dest)| {
3227 let base = chunk_index * chunk_frames;
3228 for (offset, slot) in dest.chunks_exact_mut(stride).enumerate() {
3229 let index = base + offset;
3230 if index < frames.len() {
3231 write(&frames[index], slot);
3232 }
3233 }
3234 });
3235 };
3236 if rayon::current_thread_index().is_some() {
3240 first_touch_f64(&mut values, workers);
3241 copy_chunks(&mut values);
3242 } else {
3243 first_touch_f64(&mut values, workers);
3244 std::thread::scope(|scope| {
3245 let mut rest = &mut values[..];
3246 let mut base = 0usize;
3247 let mut handles = Vec::new();
3248 while base < frames.len() {
3249 let take_frames = chunk_frames.min(frames.len() - base);
3250 let take_vals = take_frames * stride;
3251 let (dest, tail) = rest.split_at_mut(take_vals);
3252 rest = tail;
3253 let frame_slice = &frames[base..base + take_frames];
3254 handles.push(scope.spawn(move || {
3255 for (frame, slot) in
3256 frame_slice.iter().zip(dest.chunks_exact_mut(stride))
3257 {
3258 write(frame, slot);
3259 }
3260 }));
3261 base += take_frames;
3262 }
3263 for handle in handles {
3264 handle.join().expect("slab worker");
3265 }
3266 });
3267 }
3268 return Some(values);
3269 }
3270 }
3271 #[cfg(not(feature = "parallel"))]
3272 {
3273 let _ = workers;
3274 }
3275 for (frame, dest) in frames.iter().zip(values.chunks_exact_mut(stride)) {
3276 write(frame, dest);
3277 }
3278 Some(values)
3279}
3280
3281fn mix_touch(hash: u64, value: u64) -> u64 {
3282 hash.wrapping_mul(0x9E3779B185EBCA87) ^ value
3283}
3284
3285fn one_frame_touch(frame: &types::ConFrame, coords: &[f64], index: u64) -> u64 {
3286 let mut hash = index;
3287 for value in frame.header.boxl {
3288 hash = mix_touch(hash, value.to_bits());
3289 }
3290 for atom in &frame.atom_data {
3291 hash = mix_touch(hash, atom.symbol.len() as u64);
3292 for byte in atom.symbol.as_bytes() {
3293 hash = mix_touch(hash, u64::from(*byte));
3294 }
3295 }
3296 for value in coords {
3297 hash = mix_touch(hash, value.to_bits());
3298 }
3299 hash
3300}
3301
3302pub fn touch_frame_checksum(
3309 frames: &[types::ConFrame],
3310 positions: &[f64],
3311 workers: usize,
3312) -> Option<u64> {
3313 if frames.is_empty() {
3314 return Some(0);
3315 }
3316 let n_atoms = frames[0].atom_data.len();
3317 let stride = n_atoms.saturating_mul(3);
3318 if stride == 0 || positions.len() != frames.len().saturating_mul(stride) {
3319 return None;
3320 }
3321 if frames.iter().any(|frame| frame.atom_data.len() != n_atoms) {
3322 return None;
3323 }
3324 let one = |index: usize| {
3325 let start = index * stride;
3326 one_frame_touch(
3327 &frames[index],
3328 &positions[start..start + stride],
3329 index as u64,
3330 )
3331 };
3332 Some(reduce_touch(frames.len(), workers, one))
3333}
3334
3335pub fn touch_option_checksum(
3339 frames: &[Option<types::ConFrame>],
3340 positions: &[f64],
3341 workers: usize,
3342) -> Option<u64> {
3343 if frames.is_empty() {
3344 return Some(0);
3345 }
3346 if frames.iter().any(Option::is_none) {
3347 return None;
3348 }
3349 let n_atoms = frames[0].as_ref()?.atom_data.len();
3350 let stride = n_atoms.saturating_mul(3);
3351 if stride == 0 || positions.len() != frames.len().saturating_mul(stride) {
3352 return None;
3353 }
3354 if frames
3355 .iter()
3356 .any(|frame| frame.as_ref().map(|item| item.atom_data.len()) != Some(n_atoms))
3357 {
3358 return None;
3359 }
3360 let one = |index: usize| {
3361 let start = index * stride;
3362 one_frame_touch(
3363 frames[index].as_ref().expect("slot"),
3364 &positions[start..start + stride],
3365 index as u64,
3366 )
3367 };
3368 Some(reduce_touch(frames.len(), workers, one))
3369}
3370
3371fn reduce_touch(n: usize, workers: usize, one: impl Fn(usize) -> u64 + Sync) -> u64 {
3372 #[cfg(feature = "parallel")]
3373 if workers >= 2 {
3374 let fold_range = |first: usize, last: usize| {
3375 let mut acc = 0u64;
3376 for index in first..last {
3377 acc ^= one(index);
3378 }
3379 acc
3380 };
3381 let chunk = n.div_ceil(workers).max(1);
3382 if rayon::current_thread_index().is_some() {
3385 use rayon::prelude::*;
3386 return (0..workers)
3387 .into_par_iter()
3388 .map(|worker| {
3389 let first = worker * chunk;
3390 let last = (first + chunk).min(n);
3391 fold_range(first, last)
3392 })
3393 .reduce(|| 0u64, |left, right| left ^ right);
3394 }
3395 let mut acc = 0u64;
3396 std::thread::scope(|scope| {
3397 let mut handles = Vec::with_capacity(workers);
3398 for worker in 0..workers {
3399 handles.push(scope.spawn(move || {
3400 let first = worker * chunk;
3401 let last = (first + chunk).min(n);
3402 fold_range(first, last)
3403 }));
3404 }
3405 for handle in handles {
3406 acc ^= handle.join().unwrap_or(0);
3407 }
3408 });
3409 return acc;
3410 }
3411 #[cfg(not(feature = "parallel"))]
3412 {
3413 let _ = workers;
3414 }
3415 (0..n).map(one).fold(0, |acc, item| acc ^ item)
3416}
3417
3418pub fn measure_newline_scan(text: &str, threads: usize, chunked: bool) -> (Vec<usize>, f64) {
3426 #[cfg(feature = "parallel")]
3427 {
3428 if chunked && threads >= 2 {
3429 let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build();
3430 let t_scan = std::time::Instant::now();
3431 let offsets = match pool {
3432 Ok(pool) => pool.install(|| {
3433 let newlines = newline_positions(text.as_bytes(), threads);
3434 frame_start_offsets_indexed(text, newlines)
3435 }),
3436 Err(_) => {
3437 let newlines = newline_positions(text.as_bytes(), threads);
3438 frame_start_offsets_indexed(text, newlines)
3439 }
3440 };
3441 return (offsets, t_scan.elapsed().as_secs_f64());
3442 }
3443 }
3444 #[cfg(not(feature = "parallel"))]
3445 {
3446 let _ = (threads, chunked);
3447 }
3448 let t_scan = std::time::Instant::now();
3449 let offsets = frame_start_offsets_serial(text);
3450 (offsets, t_scan.elapsed().as_secs_f64())
3451}
3452
3453#[cfg(feature = "parallel")]
3456fn collect_parallel_located(
3457 file_contents: &str,
3458 num_threads: Option<usize>,
3459) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
3460 use rayon::prelude::*;
3461
3462 let boundaries = frame_start_offsets(file_contents);
3463 let parse_chunks = || -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
3464 let num_frames = boundaries.len();
3465 let parts: Vec<Result<types::ConFrame, error::LocatedParseError>> = (0..num_frames)
3466 .into_par_iter()
3467 .map(|i| {
3468 let start = boundaries[i];
3469 let end = if i + 1 < num_frames {
3470 boundaries[i + 1]
3471 } else {
3472 file_contents.len()
3473 };
3474 let chunk = &file_contents[start..end];
3475 let mut iter = ConFrameIterator::new(chunk);
3476 match iter.next() {
3477 Some(Ok(frame)) => Ok(frame),
3478 Some(Err(e)) => {
3479 let local = iter.lines.line_no();
3480 let base = file_line_at(file_contents, start);
3481 let cursor = if local == 0 { base } else { base + local - 1 };
3482 Err(error::LocatedParseError::new(e, cursor, i as u64))
3483 }
3484 None => Err(error::LocatedParseError::new(
3485 error::ParseError::IncompleteFrame,
3486 file_line_at(file_contents, start),
3487 i as u64,
3488 )),
3489 }
3490 })
3491 .collect();
3492 let mut frames = Vec::with_capacity(parts.len());
3493 for part in parts {
3494 frames.push(part?);
3495 }
3496 Ok(frames)
3497 };
3498 match num_threads {
3499 None => parse_chunks(),
3500 Some(n) => {
3501 let n = n.max(1);
3502 match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
3503 Ok(pool) => pool.install(parse_chunks),
3504 Err(_) => parse_chunks(),
3505 }
3506 }
3507 }
3508}
3509
3510#[cfg(all(test, feature = "parallel"))]
3511mod parallel_strong_scale_tests {
3512 use super::*;
3513 use std::path::PathBuf;
3514
3515 fn multi_frame_fixture() -> String {
3516 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con");
3517 let one = std::fs::read_to_string(p).expect("fixture");
3518 one.repeat(8)
3520 }
3521
3522 fn sequential_frames(text: &str) -> Vec<types::ConFrame> {
3523 ConFrameIterator::new(text)
3524 .map(|r| r.expect("seq frame"))
3525 .collect()
3526 }
3527
3528 fn frames_payload_key(f: &types::ConFrame) -> (usize, Vec<(String, f64, f64, f64)>) {
3529 let atoms: Vec<_> = f
3530 .atom_data
3531 .iter()
3532 .map(|a| (a.symbol.to_string(), a.x, a.y, a.z))
3533 .collect();
3534 (f.atom_data.len(), atoms)
3535 }
3536
3537 #[test]
3538 fn parallel_workers_match_sequential_payloads() {
3539 let text = multi_frame_fixture();
3540 let seq = sequential_frames(&text);
3541 assert!(seq.len() >= 8);
3542 let seq_keys: Vec<_> = seq.iter().map(frames_payload_key).collect();
3543
3544 for workers in [1usize, 2, 4] {
3545 let par = parse_frames_parallel_with_threads(&text, Some(workers));
3546 assert_eq!(par.len(), seq.len(), "workers={workers}");
3547 let par_keys: Vec<_> = par
3548 .into_iter()
3549 .map(|r| frames_payload_key(&r.expect("par frame")))
3550 .collect();
3551 assert_eq!(par_keys, seq_keys, "workers={workers} frame payloads");
3552 }
3553
3554 let par_default = parse_frames_parallel(&text);
3556 let def_keys: Vec<_> = par_default
3557 .into_iter()
3558 .map(|r| frames_payload_key(&r.expect("par")))
3559 .collect();
3560 assert_eq!(def_keys, seq_keys);
3561 }
3562
3563 fn live_threads() -> usize {
3564 std::fs::read_to_string("/proc/self/status")
3565 .unwrap()
3566 .lines()
3567 .find(|line| line.starts_with("Threads:"))
3568 .unwrap()
3569 .split_whitespace()
3570 .nth(1)
3571 .unwrap()
3572 .parse()
3573 .unwrap()
3574 }
3575
3576 #[test]
3578 fn default_read_of_a_wide_file_stays_on_the_calling_thread() {
3579 const CHILD: &str = "READCON_DEFAULT_THREAD_CHILD";
3580 const NAME: &str = "iterators::parallel_strong_scale_tests::default_read_of_a_wide_file_stays_on_the_calling_thread";
3581 if std::env::var_os(CHILD).is_none() {
3582 let out = std::process::Command::new(std::env::current_exe().unwrap())
3583 .args([NAME, "--exact", "--test-threads=1"])
3584 .env(CHILD, "1")
3585 .output()
3586 .unwrap();
3587 let stdout = String::from_utf8_lossy(&out.stdout);
3588 assert!(
3589 out.status.success() && stdout.contains("test result: ok. 1 passed;"),
3590 "{stdout}{}",
3591 String::from_utf8_lossy(&out.stderr)
3592 );
3593 return;
3594 }
3595 let dir = tempfile::tempdir().unwrap();
3596 let path = dir.path().join("wide.con");
3597 let one = std::fs::read_to_string(
3598 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con"),
3599 )
3600 .unwrap();
3601 std::fs::write(&path, one.repeat(200)).unwrap();
3602 assert!(std::fs::metadata(&path).unwrap().len() > 64 * 1024);
3603 let before = live_threads();
3604 let frames = super::read_all_frames(&path).unwrap();
3605 let after = live_threads();
3606 assert_eq!(before, after, "default read started worker threads");
3607 assert!(frames.len() > 1);
3608 let text = std::fs::read_to_string(&path).unwrap();
3609 let sequential = super::frames_from_text(&text, None).unwrap();
3610 assert_eq!(sequential.len(), frames.len());
3611 assert_eq!(
3612 sequential[0].atom_data[0].x, frames[0].atom_data[0].x,
3613 "default read changed a coordinate"
3614 );
3615 }
3616}
3617
3618#[cfg(all(test, feature = "parallel"))]
3619mod parallel_header_scan_tests {
3620 use super::*;
3621 use std::path::PathBuf;
3622
3623 fn fixture(name: &str) -> String {
3624 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3625 .join("resources/test")
3626 .join(name);
3627 std::fs::read_to_string(path).expect(name)
3628 }
3629
3630 fn assert_crlf_gap_cut(text: &str) {
3632 let bytes = text.as_bytes();
3633 let mut found = 0usize;
3634 let mut i = 0usize;
3635 while i + 1 < bytes.len() && found < 6 {
3636 if bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
3637 let cut = i + 1;
3638 assert_eq!(bytes[cut - 1], b'\r');
3639 assert_eq!(bytes[cut], b'\n');
3640 assert_cuts_match(text, &[0, cut, bytes.len()]);
3641 found += 1;
3642 i += 2;
3643 continue;
3644 }
3645 i += 1;
3646 }
3647 assert!(found > 0, "fixture has no CRLF pair");
3648 }
3649
3650 fn assert_cuts_match(text: &str, cuts: &[usize]) {
3651 let serial_nl: Vec<usize> = memchr::memchr_iter(b'\n', text.as_bytes()).collect();
3652 let stitched = newline_positions_at_cuts(text.as_bytes(), cuts);
3653 assert_eq!(stitched, serial_nl, "newline stitch at {cuts:?}");
3654 let serial = frame_start_offsets_serial(text);
3655 let parallel = frame_start_offsets_at_cuts(text, cuts);
3656 assert_eq!(parallel, serial, "frame offsets at {cuts:?}");
3657 }
3658
3659 fn header_straddle_cuts(text: &str) -> Vec<Vec<usize>> {
3660 let serial = frame_start_offsets_serial(text);
3661 let bytes = text.as_bytes();
3662 let mut out = Vec::new();
3663 for n in [2usize, 3, 4, 8] {
3665 out.push(equal_cuts(bytes.len(), n));
3666 }
3667 for (index, &start) in serial.iter().enumerate() {
3668 let next = serial.get(index + 1).copied().unwrap_or(bytes.len());
3669 let window = super::header_window_end(bytes, start, next);
3672 let mut line = start;
3673 while line < window {
3674 let Some(rel) = memchr::memchr(b'\n', &bytes[line..window]) else {
3675 break;
3676 };
3677 let end = line + rel;
3678 if end - line > 1 {
3679 let mid = line + (end - line) / 2;
3680 assert!(mid > line && mid < end);
3682 out.push(vec![0, mid, bytes.len()]);
3683 }
3684 line = end + 1;
3685 }
3686 }
3687 out
3688 }
3689
3690 fn assert_slab(text: &str) {
3691 assert!(!text.is_empty());
3692 for cuts in header_straddle_cuts(text) {
3693 assert_cuts_match(text, &cuts);
3694 }
3695 let bare = text.trim_end_matches(['\n', '\r']);
3696 if !bare.is_empty() {
3697 assert_cuts_match(bare, &equal_cuts(bare.len(), 4));
3698 let mid = bare.len() / 2;
3699 let line_start = bare[..mid].rfind('\n').map(|i| i + 1).unwrap_or(0);
3700 let line_end = bare[mid..]
3701 .find('\n')
3702 .map(|i| mid + i)
3703 .unwrap_or(bare.len());
3704 if line_end - line_start > 1 {
3705 let cut = line_start + (line_end - line_start) / 2;
3706 assert_cuts_match(bare, &[0, cut, bare.len()]);
3707 }
3708 }
3709 let tailed = format!("{text}\n \n\t\n");
3710 assert_cuts_match(
3711 &tailed,
3712 &[0, 3, tailed.len() / 2, tailed.len() - 1, tailed.len()],
3713 );
3714 let crlf = text.replace('\n', "\r\n");
3715 assert_cuts_match(&crlf, &equal_cuts(crlf.len(), 5));
3716 assert_crlf_gap_cut(&crlf);
3717 let serial = frame_start_offsets_serial(text);
3718 if serial.len() >= 2 {
3719 let second = serial[1];
3720 let next = serial.get(2).copied().unwrap_or(text.len());
3721 let broken_at = second + (next - second) / 2;
3722 let broken = &text[..broken_at.max(1)];
3723 assert_cuts_match(broken, &[0, 1.max(broken.len() / 3), broken.len()]);
3724 }
3725 }
3726
3727 #[test]
3728 fn chunked_header_scan_matches_serial_offsets() {
3729 assert_eq!(frame_start_offsets_serial(""), frame_start_offsets(""));
3730 assert_cuts_match("\n", &[0, 1]);
3731 assert_cuts_match(" \n\n", &[0, 2, 5]);
3732 for name in [
3733 "tiny_cuh2.con",
3734 "tiny_cuh2.convel",
3735 "tiny_cuh2_forces.con",
3736 "tiny_cuh2_vel_forces.con",
3737 "cuh2.con",
3738 ] {
3739 let one = fixture(name);
3740 assert_slab(&one);
3741 assert_slab(&one.repeat(3));
3742 }
3743 let one = fixture("cuh2.con");
3747 for wide in [one.clone(), one.repeat(8)] {
3748 let serial = frame_start_offsets_serial(&wide);
3749 for workers in [2usize, 3, 4, 8] {
3750 let parallel = frame_start_offsets_with_threads(&wide, Some(workers));
3751 assert_eq!(parallel, serial, "workers={workers} bytes={}", wide.len());
3752 let parsed =
3753 frames_from_bytes(wide.as_bytes(), Some(workers)).expect("bytes parse");
3754 assert_eq!(parsed.len(), serial.len());
3755 let seq = frames_from_text(&wide, Some(1)).expect("serial parse");
3756 assert_eq!(parsed.len(), seq.len());
3757 assert_eq!(parsed[0].atom_data[0].x, seq[0].atom_data[0].x);
3758 assert_eq!(
3759 parsed.last().unwrap().atom_data.last().unwrap().z,
3760 seq.last().unwrap().atom_data.last().unwrap().z
3761 );
3762 }
3763 }
3764 }
3765}
3766
3767#[cfg(all(test, feature = "parallel"))]
3772fn header_window_end(bytes: &[u8], start: usize, limit: usize) -> usize {
3773 let limit = limit.min(bytes.len());
3774 let marker = b"Coordinates";
3775 let mut line = start;
3776 while line < limit {
3777 let rest = &bytes[line..limit];
3778 let rel = memchr::memchr(b'\n', rest).unwrap_or(rest.len());
3779 let content_end = line + rel;
3780 let content = &bytes[line..content_end];
3781 let trimmed = match content.iter().position(|byte| !byte.is_ascii_whitespace()) {
3782 Some(index) => &content[index..],
3783 None => b"",
3784 };
3785 if trimmed.starts_with(marker) {
3786 return line;
3787 }
3788 if content_end >= limit {
3789 break;
3790 }
3791 line = content_end + 1;
3792 }
3793 limit
3794}
3795
3796#[cfg(all(test, feature = "parallel"))]
3797mod parallel_frame_parse_tests {
3798 use super::*;
3799 use std::path::PathBuf;
3800
3801 const ZERO_ATOM: &str = "\
3802zero-atom frame
3803{\"con_spec_version\":3,\"con_spec_minor\":1,\"units\":{\"length\":\"angstrom\",\"energy\":\"eV\"}}
380410.0 10.0 10.0
380590.0 90.0 90.0
3806
3807
38080
3809
3810
3811";
3812
3813 fn fixture(name: &str) -> String {
3814 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3815 .join("resources/test")
3816 .join(name);
3817 std::fs::read_to_string(path).expect(name)
3818 }
3819
3820 fn serial_results(text: &str) -> Vec<Result<types::ConFrame, error::ParseError>> {
3821 ConFrameIterator::new(text).collect()
3822 }
3823
3824 fn written(frame: &types::ConFrame) -> Vec<u8> {
3825 let mut out = Vec::new();
3826 {
3827 let mut writer = crate::writer::ConFrameWriter::new(&mut out);
3828 writer.write_frame(frame).expect("write");
3829 writer.flush().expect("flush");
3830 }
3831 out
3832 }
3833
3834 fn assert_same_results(
3835 label: &str,
3836 serial: Vec<Result<types::ConFrame, error::ParseError>>,
3837 parallel: Vec<Result<types::ConFrame, error::ParseError>>,
3838 ) {
3839 assert_eq!(serial.len(), parallel.len(), "{label} count");
3840 for (i, (left, right)) in serial.into_iter().zip(parallel).enumerate() {
3841 match (left, right) {
3842 (Ok(a), Ok(b)) => {
3843 assert_eq!(written(&a), written(&b), "{label} bytes frame {i}");
3844 assert_eq!(a, b, "{label} fields frame {i}");
3845 }
3846 (Err(a), Err(b)) => {
3847 assert_eq!(
3848 format!("{a:?}"),
3849 format!("{b:?}"),
3850 "{label} error frame {i}"
3851 );
3852 }
3853 (Ok(_), Err(err)) => panic!("{label} frame {i} serial ok parallel {err:?}"),
3854 (Err(err), Ok(_)) => panic!("{label} frame {i} serial {err:?} parallel ok"),
3855 }
3856 }
3857 }
3858
3859 fn assert_workers(label: &str, text: &str) {
3860 let serial_ok = serial_results(text).iter().all(|item| item.is_ok());
3861 for workers in [1usize, 2, 3, 4, 8] {
3862 let parallel = parse_frames_parallel_with_threads(text, Some(workers));
3863 assert_same_results(
3864 &format!("{label} workers {workers}"),
3865 serial_results(text),
3866 parallel,
3867 );
3868 let bounds = collect_frame_starts(ConFrameIterator::new(text), true);
3869 let at = parse_frames_at_offsets(text, &bounds, Some(workers));
3870 assert_same_results(
3871 &format!("{label} offsets workers {workers}"),
3872 serial_results(text),
3873 at,
3874 );
3875 }
3876 if serial_ok {
3877 for workers in [2usize, 3, 8] {
3878 let bytes = frames_from_bytes(text.as_bytes(), Some(workers)).expect(label);
3879 let seq = frames_from_text(text, Some(1)).expect(label);
3880 assert_eq!(bytes.len(), seq.len(), "{label} byte count");
3881 for (i, (a, b)) in bytes.iter().zip(seq.iter()).enumerate() {
3882 assert_eq!(written(a), written(b), "{label} byte path frame {i}");
3883 assert_eq!(a, b, "{label} byte path fields frame {i}");
3884 }
3885 }
3886 }
3887 }
3888
3889 fn crlf_gap_offsets(text: &str) -> Vec<usize> {
3890 let bytes = text.as_bytes();
3891 let mut cut = None;
3892 let mut i = 0usize;
3893 while i + 1 < bytes.len() {
3894 if bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
3895 cut = Some(i + 1);
3896 break;
3897 }
3898 i += 1;
3899 }
3900 let cut = cut.expect("crlf");
3901 frame_start_offsets_at_cuts(text, &[0, cut, bytes.len()])
3902 }
3903
3904 #[test]
3905 fn parallel_slots_match_serial_frames() {
3906 assert_workers("empty", "");
3907 assert_workers("blank", "\n");
3908 assert_workers("space", " \n\n");
3909 assert_workers("zero", ZERO_ATOM);
3910 assert_workers("zero-repeat", &ZERO_ATOM.repeat(5));
3911 for name in [
3912 "tiny_cuh2.con",
3913 "tiny_cuh2.convel",
3914 "tiny_cuh2_forces.con",
3915 "tiny_cuh2_vel_forces.con",
3916 "tiny_cuh2_charges_spins_magmoms.con",
3917 "tiny_cuh2_displacements.con",
3918 "tiny_cuh2_spreads.con",
3919 "cuh2.con",
3920 ] {
3921 let one = fixture(name);
3922 assert_workers(name, &one);
3923 assert_workers(&format!("{name} x5"), &one.repeat(5));
3924 let bare = one.trim_end_matches(['\n', '\r']);
3925 assert_workers(&format!("{name} bare"), bare);
3926 let tailed = format!("{one}\n \n\t\n");
3927 assert_workers(&format!("{name} tail"), &tailed);
3928 let crlf = one.replace('\n', "\r\n");
3929 assert_workers(&format!("{name} crlf"), &crlf);
3930 let gap = crlf_gap_offsets(&crlf);
3931 assert_eq!(
3932 gap,
3933 frame_start_offsets_serial(&crlf),
3934 "{name} crlf gap offsets"
3935 );
3936 let parsed = parse_frames_at_offsets(&crlf, &gap, Some(4));
3937 assert_same_results(
3938 &format!("{name} crlf gap parse"),
3939 serial_results(&crlf),
3940 parsed,
3941 );
3942 let serial_offs = frame_start_offsets_serial(&one);
3943 let broken: &str = if serial_offs.len() >= 2 {
3944 let second = serial_offs[1];
3945 let next = serial_offs.get(2).copied().unwrap_or(one.len());
3946 &one[..second + (next - second) / 2]
3947 } else if one.len() > 8 {
3948 &one[..one.len() / 2]
3949 } else {
3950 ""
3951 };
3952 if !broken.is_empty() {
3953 let serial_err = frames_from_text(broken, Some(1))
3954 .err()
3955 .map(|e| format!("{e:?}"));
3956 assert!(serial_err.is_some(), "{name} chopped still parsed");
3957 for workers in [1usize, 2, 4, 8] {
3958 let parallel_err = frames_from_text(broken, Some(workers))
3959 .err()
3960 .map(|e| format!("{e:?}"));
3961 assert_eq!(parallel_err, serial_err, "{name} chopped workers {workers}");
3962 let byte_err = frames_from_bytes(broken.as_bytes(), Some(workers))
3963 .err()
3964 .map(|e| format!("{e:?}"));
3965 assert_eq!(
3966 byte_err, serial_err,
3967 "{name} chopped bytes workers {workers}"
3968 );
3969 }
3970 }
3971 }
3972 }
3973}
3974
3975#[cfg(all(test, feature = "parallel"))]
3976mod phased_read_tests {
3977 use super::*;
3978 use std::path::PathBuf;
3979
3980 fn fixture(name: &str) -> String {
3981 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3982 .join("resources/test")
3983 .join(name);
3984 std::fs::read_to_string(path).expect(name)
3985 }
3986
3987 #[test]
3988 fn phased_bytes_match_serial_frames() {
3989 let text = fixture("cuh2.con").repeat(4);
3990 let serial = frames_from_text(&text, Some(1)).expect("serial");
3991 for workers in [1usize, 2, 4, 8] {
3992 let phased = frames_from_bytes_phased(text.as_bytes(), Some(workers)).expect("phased");
3993 let parallel = phased.frames;
3994 assert_eq!(parallel.len(), serial.len(), "workers={workers}");
3995 assert!(
3996 phased.scan_s >= 0.0 && phased.slot_alloc_s >= 0.0 && phased.worker_parse_s >= 0.0
3997 );
3998 assert_eq!(parallel[0], serial[0], "workers={workers} first");
3999 assert_eq!(
4000 parallel.last().unwrap(),
4001 serial.last().unwrap(),
4002 "workers={workers} last"
4003 );
4004 let slab = pack_position_slab(&serial, workers).expect("slab");
4005 let stride = serial[0].positions.nrows() * 3;
4006 let src = serial[0].positions.f64_slice().expect("f64");
4007 assert_eq!(&slab[..stride], src);
4008 let (chunked, _) = measure_newline_scan(&text, workers.max(2), true);
4009 let (plain, _) = measure_newline_scan(&text, 1, false);
4010 if workers >= 2 {
4011 assert_eq!(chunked, plain);
4012 }
4013 }
4014 }
4015
4016 #[test]
4017 fn phased_file_matches_serial_and_maps_a_wide_file() {
4018 let text = fixture("cuh2.con").repeat(5);
4019 let dir = std::env::temp_dir();
4020 let path = dir.join("readcon-phased-wide.con");
4021 std::fs::write(&path, &text).expect("write");
4022 let phased = read_all_frames_phased(&path, Some(4)).expect("phased");
4023 let serial = frames_from_text(&text, Some(1)).expect("serial");
4024 assert_eq!(phased.frames, serial);
4025 assert!(phased.phases.mapped, "a file past 64 KiB is mapped");
4026 assert!(phased.phases.worker_parse_s >= 0.0);
4027 assert_eq!(phased.n_bytes, text.len());
4028 let _ = std::fs::remove_file(&path);
4029 }
4030
4031 #[test]
4032 fn equal_span_boundaries_match_the_serial_walk() {
4033 let text = fixture("cuh2.con").repeat(8);
4034 let bytes = text.as_bytes();
4035 let got = equal_span_boundaries(bytes, 4).expect("equal span");
4036 let expect = collect_frame_starts(ConFrameIterator::from_bytes(bytes), true);
4037 assert_eq!(got, expect);
4038 assert_eq!(got.len(), 8);
4039 let phased = frames_from_bytes_phased(bytes, Some(4)).expect("phased");
4040 assert_eq!(phased.scan_kind, SCAN_EQUAL_SPAN);
4041 assert_eq!(
4042 phased.frames,
4043 frames_from_text(&text, Some(1)).expect("serial")
4044 );
4045 let tailed = format!("{text}\n\n \n");
4046 let tail_phased = frames_from_bytes_phased(tailed.as_bytes(), Some(4)).expect("tail");
4047 assert_eq!(tail_phased.scan_kind, SCAN_EQUAL_SPAN);
4048 assert_eq!(
4049 tail_phased.frames,
4050 frames_from_text(&tailed, Some(1)).expect("tail serial")
4051 );
4052 }
4053
4054 #[test]
4055 fn a_whitespace_tail_uses_the_equal_span_walk() {
4056 let mut text = fixture("cuh2.con").repeat(3);
4057 text.push('\n');
4058 assert!(equal_span_boundaries(text.as_bytes(), 4).is_some());
4059 let serial = frames_from_text(&text, Some(1)).expect("serial");
4060 let parallel = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("phased");
4061 assert_eq!(parallel.scan_kind, SCAN_EQUAL_SPAN);
4062 assert_eq!(parallel.frames, serial);
4063 }
4064
4065 #[test]
4066 fn touch_checksum_matches_across_worker_counts() {
4067 let text = fixture("cuh2.con").repeat(4);
4068 let serial = frames_from_bytes_phased(text.as_bytes(), Some(1)).expect("serial");
4069 let parallel = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("parallel");
4070 let slab_s = pack_position_slab(&serial.frames, 1).expect("slab");
4071 let slab_p = pack_position_slab(¶llel.frames, 4).expect("slab");
4072 let left = touch_frame_checksum(&serial.frames, &slab_s, 1).expect("touch");
4073 let right = touch_frame_checksum(¶llel.frames, &slab_p, 4).expect("touch");
4074 assert_eq!(left, right);
4075 assert_ne!(left, 0);
4076 }
4077}
4078
4079#[cfg(test)]
4080mod later_frame_location_tests {
4081 use super::*;
4082 use std::fs;
4083
4084 fn one_atom(coord: &str) -> String {
4085 format!(
4086 "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"
4087 )
4088 }
4089
4090 fn bad_count_frame() -> String {
4091 "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()
4092 }
4093
4094 fn two_frames() -> String {
4095 format!("{}{}", one_atom("0 0 0 0 0"), bad_count_frame())
4096 }
4097
4098 fn assert_later_count(
4099 err: &error::ParseError,
4100 line: u32,
4101 frame: u64,
4102 iter: &ConFrameIterator<'_>,
4103 ) {
4104 assert_eq!(err.class_name(), "InvalidCount", "{err}");
4105 assert_eq!(iter.lines.line_no(), line, "{err}");
4106 assert_eq!(iter.frame_index(), frame, "{err}");
4107 }
4108
4109 fn assert_first_pass(err: Box<dyn std::error::Error>) {
4110 let located = err
4111 .downcast_ref::<error::LocatedParseError>()
4112 .unwrap_or_else(|| panic!("first pass lost the location: {err}"));
4113 assert_eq!(located.error.class_name(), "InvalidCount", "{located:?}");
4114 assert_eq!(located.line, 20, "{located:?}");
4115 assert_eq!(located.frame_index, 1, "{located:?}");
4116 }
4117
4118 fn take_err<T>(result: Result<T, Box<dyn std::error::Error>>) -> Box<dyn std::error::Error> {
4119 match result {
4120 Err(err) => err,
4121 Ok(_) => panic!("expected a parse error"),
4122 }
4123 }
4124
4125 #[test]
4128 fn batch_readers_keep_the_later_frame_on_the_first_pass() {
4129 let path = std::env::temp_dir().join(format!(
4130 "readcon-later-frame-first-{}.con",
4131 std::process::id()
4132 ));
4133 fs::write(&path, two_frames()).unwrap();
4134 for threads in [None, Some(1), Some(2)] {
4135 assert_first_pass(take_err(read_all_frames_with_threads(&path, threads)));
4136 assert_first_pass(take_err(read_all_frames_phased(&path, threads)));
4137 assert_first_pass(take_err(read_positions_with_threads(&path, threads)));
4138 }
4139 let _ = fs::remove_file(&path);
4140 }
4141
4142 #[test]
4144 fn next_forward_and_skip_locate_the_later_frame() {
4145 let text = two_frames();
4146
4147 let mut iter = ConFrameIterator::new(&text);
4148 assert!(iter.next().unwrap().is_ok());
4149 let err = iter.next().unwrap().unwrap_err();
4150 assert_later_count(&err, 20, 1, &iter);
4151
4152 let mut iter = ConFrameIterator::new(&text);
4153 assert!(iter.forward().unwrap().is_ok());
4154 let err = iter.forward().unwrap().unwrap_err();
4155 assert_later_count(&err, 20, 1, &iter);
4156
4157 let mut iter = ConFrameIterator::new(&text);
4158 let err = iter.skip_frames(3).unwrap_err();
4159 assert_later_count(&err, 20, 1, &iter);
4160 }
4161
4162 #[test]
4166 fn skip_after_a_velocity_frame_keeps_the_separator_line() {
4167 let text = format!(
4168 "{}\nH\nVelocities of Component 1\n0 0 0 0 0\n{}",
4169 one_atom("0 0 0 0 0"),
4170 bad_count_frame()
4171 );
4172 let mut iter = ConFrameIterator::new(&text);
4173 assert!(iter.forward().unwrap().is_ok());
4174 let err = iter.forward().unwrap().unwrap_err();
4175 assert_later_count(&err, 24, 1, &iter);
4176 }
4177
4178 #[test]
4179 fn count_frames_locates_the_later_frame() {
4180 let path = std::env::temp_dir().join(format!(
4181 "readcon-later-frame-count-{}.con",
4182 std::process::id()
4183 ));
4184 fs::write(&path, two_frames()).unwrap();
4185 let err = count_frames(&path).unwrap_err();
4186 let _ = fs::remove_file(&path);
4187 match err {
4188 LoadError::Parse(located) => {
4189 assert_eq!(located.error.class_name(), "InvalidCount");
4190 assert_eq!(located.line, 20, "{located}");
4191 assert_eq!(located.frame_index, 1, "{located}");
4192 }
4193 other => panic!("expected a located parse error, got {other}"),
4194 }
4195 }
4196
4197 #[test]
4200 fn metadata_in_a_later_frame_reports_that_frames_line() {
4201 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";
4202 let text = format!("{}{}", one_atom("0 0 0 0 0"), bad);
4203 for threads in [Some(1usize), Some(2)] {
4204 let err = frames_from_text_located(&text, threads).unwrap_err();
4205 assert_eq!(
4206 err.error.class_name(),
4207 "InvalidMetadataJson",
4208 "{threads:?}: {err}"
4209 );
4210 assert_eq!(err.frame_index, 1, "{threads:?}");
4211 assert_eq!(err.line, 14, "{threads:?}: {err}");
4212 }
4213 }
4214
4215 #[test]
4223 fn parallel_positions_report_the_earliest_failing_slot() {
4224 const SLOW_ATOMS: usize = 20_000;
4225 let slow = counted_frame(SLOW_ATOMS);
4226 let bad = counted_frame(1);
4227 let text = format!("{slow}{bad}{bad}{bad}");
4228 let earliest_line = 1 + slow.bytes().filter(|byte| *byte == b'\n').count() as u32;
4229 let path =
4230 std::env::temp_dir().join(format!("readcon-earliest-slot-{}.con", std::process::id()));
4231 fs::write(&path, &text).unwrap();
4232 assert_earliest_slot(
4233 take_err(read_positions_with_threads(&path, Some(1))),
4234 earliest_line,
4235 );
4236 assert_earliest_slot(
4237 take_err(read_positions_with_threads(&path, Some(2))),
4238 earliest_line,
4239 );
4240 let _ = fs::remove_file(&path);
4241 }
4242
4243 fn counted_frame(natoms: usize) -> String {
4244 let mut body = String::with_capacity(natoms * 10 + 96);
4245 body.push_str("seed\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\n0 0\n0 0 0\n1\n");
4246 body.push_str(&natoms.to_string());
4247 body.push_str("\n1.0\nH\nCoordinates of Component 1\n");
4248 for _ in 0..natoms {
4249 body.push_str("0 0 0 0 0\n");
4250 }
4251 body
4252 }
4253
4254 fn assert_earliest_slot(err: Box<dyn std::error::Error>, line: u32) {
4255 let located = err
4256 .downcast_ref::<error::LocatedParseError>()
4257 .unwrap_or_else(|| panic!("position read lost the location: {err}"));
4258 assert_eq!(located.error.class_name(), "InvalidCount", "{located:?}");
4259 assert_eq!(located.frame_index, 1, "{located:?}");
4260 assert_eq!(located.line, line, "{located:?}");
4261 }
4262}
4263
4264#[cfg(test)]
4265mod frame_equality_tests {
4266 use super::ConFrameIterator;
4267
4268 fn sample_con(frames: usize, long: bool) -> String {
4269 let mut out = String::new();
4270 for f in 0..frames {
4271 let shift = (f as f64) * 1.0e-4;
4272 out.push_str("Generated by benchmark\n");
4273 out.push_str("{\"con_spec_version\":2}\n");
4274 out.push_str("15.3456 21.702 100.0\n");
4275 out.push_str("90.0 90.0 90.0\n");
4276 out.push_str("0 0\n");
4277 out.push_str(&format!("{f} 0 1\n"));
4278 out.push_str("2\n6 2\n63.546 1.00793\n");
4279 out.push_str("Cu\nCoordinates of Component 1\n");
4280 for i in 0..6 {
4281 let x = (i as f64) * 2.557 + 0.6394 + shift;
4282 let y = 0.9045 + shift * 0.5;
4283 let z = (f as f64) * 1.0e-6 + if i == 3 { -1.25 } else { 0.0 };
4284 if long {
4285 out.push_str(&format!(" {x:.17} {y:.17} {z:.17} 0 {i}\n"));
4286 } else {
4287 out.push_str(&format!(" {x:.6} {y:.6} {z:.6} 0 {i}\n"));
4288 }
4289 }
4290 out.push_str("H\nCoordinates of Component 2\n");
4291 out.push_str(" 8.6823 9.947 11.733 0 6\n");
4292 out.push_str(&format!(" -7.9421 9.947 {shift:.6} 0 7\n"));
4293 }
4294 out
4295 }
4296
4297 fn sample_convel() -> String {
4298 let mut out = String::new();
4299 for f in 0..3 {
4300 let shift = (f as f64) * 1.0e-4;
4301 out.push_str("Generated by benchmark\n");
4302 out.push_str("{\"con_spec_version\":2}\n");
4303 out.push_str("15.3456 21.702 100.0\n");
4304 out.push_str("90.0 90.0 90.0\n");
4305 out.push_str("0 0\n");
4306 out.push_str(&format!("{f} 0 1\n"));
4307 out.push_str("2\n4 1\n63.546 1.00793\n");
4308 out.push_str("Cu\nCoordinates of Component 1\n");
4309 for i in 0..4 {
4310 let x = (i as f64) * 2.557 + shift;
4311 out.push_str(&format!(" {x:.17} 0.9045 1.0 0 {i}\n"));
4312 }
4313 out.push_str("H\nCoordinates of Component 2\n");
4314 out.push_str(" 1.5 2.5 3.5 0 4\n");
4315 out.push_str("\nCu\nVelocities of Component 1\n");
4316 for i in 0..4 {
4317 let vx = (i as f64) * 1.0e-4 - shift;
4318 out.push_str(&format!(" {vx:.17} -0.5 0.25 0 {i}\n"));
4319 }
4320 out.push_str("H\nVelocities of Component 2\n");
4321 out.push_str(" 0.1 -0.2 0.3 0 4\n");
4322 }
4323 out
4324 }
4325
4326 fn reference_f64(token: &str) -> f64 {
4327 let (val, n) = fast_float2::parse_partial::<f64, _>(token).expect(token);
4328 assert_eq!(n, token.len(), "{token}");
4329 val
4330 }
4331
4332 fn assert_rows_match(text: &str, positions: bool) {
4333 let frames: Vec<_> = ConFrameIterator::new(text)
4334 .map(|frame| frame.expect("parse"))
4335 .collect();
4336 assert!(frames.len() >= 2);
4337 let mut frame_i = 0usize;
4338 let mut row = 0usize;
4339 let mut in_section = false;
4340 for line in text.lines() {
4341 let token = line.trim();
4342 if token.is_empty() {
4343 in_section = true;
4344 row = 0;
4345 continue;
4346 }
4347 if token.starts_with("Generated") {
4348 if row > 0 || in_section {
4349 frame_i += 1;
4350 }
4351 row = 0;
4352 in_section = false;
4353 continue;
4354 }
4355 let cols: Vec<&str> = token.split_whitespace().collect();
4356 if cols.len() < 4 || reference_ok(cols[0]).is_none() {
4357 continue;
4358 }
4359 if cols[3].parse::<u64>().is_err() {
4360 continue;
4361 }
4362 let got = if in_section {
4363 frames[frame_i].atom_data[row].velocity().expect("velocity")
4364 } else if positions {
4365 let p = frames[frame_i].positions.as_f64_row(row);
4366 [p[0], p[1], p[2]]
4367 } else {
4368 continue;
4369 };
4370 for (k, token) in cols.iter().take(3).enumerate() {
4371 assert_eq!(
4372 got[k].to_bits(),
4373 reference_f64(token).to_bits(),
4374 "frame {frame_i} row {row} col {k} token {token} section {in_section}"
4375 );
4376 }
4377 let aos = if in_section {
4378 frames[frame_i].atom_data[row].velocity().unwrap()
4379 } else {
4380 let atom = &frames[frame_i].atom_data[row];
4381 [atom.x, atom.y, atom.z]
4382 };
4383 for k in 0..3 {
4384 assert_eq!(aos[k].to_bits(), got[k].to_bits());
4385 }
4386 row += 1;
4387 }
4388 }
4389
4390 fn reference_ok(token: &str) -> Option<f64> {
4391 let (val, n) = fast_float2::parse_partial::<f64, _>(token).ok()?;
4392 (n == token.len()).then_some(val)
4393 }
4394
4395 #[test]
4396 fn frames_match_general_decimal_parser() {
4397 assert_rows_match(&sample_con(4, true), true);
4398 assert_rows_match(&sample_con(4, false), true);
4399 let text = sample_convel();
4400 let frames: Vec<_> = ConFrameIterator::new(&text)
4401 .map(|frame| frame.expect("convel"))
4402 .collect();
4403 assert_eq!(frames.len(), 3);
4404 assert!(
4405 frames
4406 .iter()
4407 .all(|frame| frame.atom_data[0].velocity().is_some())
4408 );
4409 assert_eq!(frames[0].positions.nrows(), 5);
4410 let mut frame_i = 0usize;
4412 let mut coord_row = 0usize;
4413 let mut vel_row = 0usize;
4414 let mut velocities = false;
4415 for line in text.lines() {
4416 let token = line.trim();
4417 if token.starts_with("Generated") && (coord_row > 0 || velocities) {
4418 frame_i += 1;
4419 coord_row = 0;
4420 vel_row = 0;
4421 velocities = false;
4422 }
4423 if token.starts_with("Velocities") {
4424 velocities = true;
4425 continue;
4426 }
4427 let cols: Vec<&str> = token.split_whitespace().collect();
4428 if cols.len() < 5 {
4429 continue;
4430 }
4431 if reference_ok(cols[0]).is_none() || cols[3].parse::<u64>().is_err() {
4432 continue;
4433 }
4434 let row = if velocities { vel_row } else { coord_row };
4435 let got = if velocities {
4436 frames[frame_i].atom_data[row].velocity().unwrap()
4437 } else {
4438 let p = frames[frame_i].positions.as_f64_row(row);
4439 [p[0], p[1], p[2]]
4440 };
4441 for (k, col) in cols.iter().take(3).enumerate() {
4442 assert_eq!(got[k].to_bits(), reference_f64(col).to_bits(), "{token}");
4443 }
4444 if velocities {
4445 let soa = frames[frame_i].velocities.as_f64_row(row);
4446 for k in 0..3 {
4447 assert_eq!(soa[k].to_bits(), got[k].to_bits());
4448 }
4449 vel_row += 1;
4450 } else {
4451 coord_row += 1;
4452 }
4453 }
4454 assert_eq!(frame_i, 2);
4455 assert_eq!(vel_row, 5);
4456 }
4457}
4458
4459#[cfg(test)]
4460mod newline_skip_tests {
4461 use super::ConFrameIterator;
4462
4463 fn naive(bytes: &[u8], n: usize) -> Result<usize, ()> {
4466 let mut pos = 0usize;
4467 for i in 0..n {
4468 let rest = &bytes[pos..];
4469 match memchr::memchr(b'\n', rest) {
4470 Some(at) => pos += at + 1,
4471 None => {
4472 if rest.is_empty() {
4473 return Err(());
4474 }
4475 pos = bytes.len();
4476 if i + 1 == n {
4477 return Ok(pos);
4478 }
4479 return Err(());
4480 }
4481 }
4482 }
4483 Ok(pos)
4484 }
4485
4486 #[test]
4487 fn counted_skip_matches_per_line_memchr() {
4488 let mut samples = vec![
4489 String::new(),
4490 "a".into(),
4491 "a\n".into(),
4492 "a\nb".into(),
4493 "\n".into(),
4494 "\n\n".into(),
4495 "abc\ndef\nghi".into(),
4496 "a\r\nb\r\n".into(),
4497 "x".repeat(100) + "\n" + &"y".repeat(40),
4498 ];
4499 let path =
4500 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/cuh2.con");
4501 let one = std::fs::read_to_string(&path).unwrap();
4502 samples.push(one.repeat(2));
4503 let bare = one.trim_end_matches(['\n', '\r']).to_string();
4504 samples.push(bare);
4505 for text in samples {
4506 let bytes = text.as_bytes();
4507 for n in [0usize, 1, 2, 3, 6, 7, 9, 40, 200, 450, 1000, 5000] {
4508 let expect = naive(bytes, n);
4509 let mut it = ConFrameIterator::from_bytes(bytes);
4510 let got = it.advance_lines(n).map(|_| it.byte_pos()).map_err(|_| ());
4511 assert_eq!(got, expect, "n={n} len={}", bytes.len());
4512 }
4513 }
4514 }
4515}
4516
4517#[cfg(test)]
4518mod cached_unit_table_read_paths {
4519 use super::*;
4520
4521 const FRAME: &str = "\
4522v3
4523{\"con_spec_version\":3,\"con_spec_minor\":1,\"units\":{\"length\":\"angstrom\",\"energy\":\"eV\"}}
452410 10 10
452590 90 90
4526
4527
45281
45291
45301.0
4531H
4532Coordinates of Component 1
45330.5 0.25 0.125 0 7
4534";
4535
4536 fn coords(frame: &types::ConFrame) -> [f64; 3] {
4537 frame.positions.as_f64_row(0)
4538 }
4539
4540 #[test]
4543 fn read_paths_share_the_cached_unit_table() {
4544 let addr = crate::units::cached_unit_table_addr();
4545 let serial = ConFrameIterator::new(FRAME)
4546 .next()
4547 .expect("frame")
4548 .expect("parse");
4549 let from_bytes = frames_from_bytes(FRAME.as_bytes(), Some(1)).expect("bytes");
4550 let phased = frames_from_bytes_phased(FRAME.as_bytes(), Some(1)).expect("phased");
4551 assert_eq!(from_bytes.len(), 1);
4552 assert_eq!(phased.frames.len(), 1);
4553 assert_eq!(coords(&from_bytes[0]), coords(&serial));
4554 assert_eq!(coords(&phased.frames[0]), coords(&serial));
4555 assert_eq!(crate::units::cached_unit_table_addr(), addr);
4556 let bad = FRAME.replace("angstrom", "not-a-unit");
4557 assert!(ConFrameIterator::new(&bad).next().unwrap().is_err());
4558 assert!(frames_from_bytes(bad.as_bytes(), Some(1)).is_err());
4559 assert!(frames_from_bytes_phased(bad.as_bytes(), Some(1)).is_err());
4560 assert_eq!(crate::units::cached_unit_table_addr(), addr);
4561 }
4562}