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 pub(crate) fn unread_len(&self) -> usize {
131 let start = match self.peeked {
132 Some(p) => p.as_ptr() as usize - self.bytes.as_ptr() as usize,
133 None => self.pos,
134 };
135 self.bytes.len() - start
136 }
137
138 fn clear_peek(&mut self) {
140 if let Some(p) = self.peeked.take() {
141 let start = p.as_ptr() as usize - self.bytes.as_ptr() as usize;
143 self.pos = start;
144 self.line_no = self.line_no.saturating_sub(1);
148 if let Some(nl) = self.newlines.as_deref() {
149 self.index_line = line_no_for_pos(nl, self.bytes.len(), start);
150 }
151 }
152 }
153
154 fn read_one_indexed(&mut self) -> Option<&'a str> {
160 if self.utf8_bad || self.pos >= self.bytes.len() {
161 return None;
162 }
163 let len = self.bytes.len();
164 let line_no = {
165 let nl = self.newlines.as_deref().unwrap();
166 if self.pos == line_start_pos(nl, len, self.index_line) {
167 self.index_line
168 } else {
169 line_no_for_pos(nl, len, self.pos)
170 }
171 };
172 let span = indexed_span(self.newlines.as_deref().unwrap(), self.bytes, line_no)?;
173 let trimmed = &self.bytes[span.start..span.content_end];
174 if self.check_utf8 {
175 match std::str::from_utf8(trimmed) {
176 Ok(s) => {
177 self.pos = span.next_pos;
178 self.index_line = line_no + 1;
179 self.line_no = self.index_line as u32;
180 Some(s)
181 }
182 Err(_) => {
183 self.utf8_bad = true;
184 self.index_line = line_no;
185 self.line_no = (line_no as u32).saturating_add(1);
188 None
189 }
190 }
191 } else {
192 self.pos = span.next_pos;
193 self.index_line = line_no + 1;
194 self.line_no = self.index_line as u32;
195 Some(unsafe { std::str::from_utf8_unchecked(trimmed) })
197 }
198 }
199
200 fn utf8_error(&self) -> Option<error::ParseError> {
201 self.utf8_bad.then(invalid_utf8)
202 }
203
204 fn prefer_utf8(&self, err: error::ParseError) -> error::ParseError {
206 if self.utf8_bad { invalid_utf8() } else { err }
207 }
208}
209
210fn invalid_utf8() -> error::ParseError {
211 error::ParseError::ValidationError("invalid UTF-8".into())
212}
213
214impl<'a> Iterator for MemchrLines<'a> {
215 type Item = &'a str;
216 fn next(&mut self) -> Option<&'a str> {
217 self.next_line()
218 }
219}
220
221impl<'a> LineStream<'a> for MemchrLines<'a> {
222 #[inline]
223 fn next_line(&mut self) -> Option<&'a str> {
224 MemchrLines::next_line(self)
225 }
226 #[inline]
227 fn peek_line(&mut self) -> Option<&'a str> {
228 MemchrLines::peek_line(self)
229 }
230 #[inline]
231 fn next_coord_row(
232 &mut self,
233 spec_version: u32,
234 default_id: u64,
235 ) -> Result<([f64; 3], [bool; 3], u64), crate::error::ParseError> {
236 if let Some(p) = self.peeked.take() {
237 return crate::parser::parse_coord_row_at(p.as_bytes(), 0, spec_version, default_id)
238 .map(|(row, _)| row);
239 }
240 self.line_no = self.line_no.saturating_add(1);
243 let (row, pos) =
244 crate::parser::parse_coord_row_at(self.bytes, self.pos, spec_version, default_id)?;
245 self.pos = pos;
246 Ok(row)
247 }
248 fn try_take_coord_block(
249 &mut self,
250 n: usize,
251 spec_version: u32,
252 start_id: u64,
253 symbol: &std::sync::Arc<str>,
254 workers: usize,
255 ) -> Option<crate::parser::CoordRows> {
256 if self.peeked.is_some() || self.utf8_bad {
257 return None;
258 }
259 #[cfg(feature = "parallel")]
260 {
261 let rest = &self.bytes[self.pos..];
262 let block = crate::parser::parallel_coord_rows(
263 rest,
264 n,
265 spec_version,
266 start_id,
267 workers,
268 Some(symbol),
269 )
270 .ok()?;
271 self.pos += block.consumed;
272 self.line_no = self.line_no.saturating_add(n as u32);
273 if self.newlines.is_some() {
274 self.index_line = self.index_line.saturating_add(n);
275 }
276 return Some(block);
277 }
278 #[cfg(not(feature = "parallel"))]
279 {
280 let _ = (n, spec_version, start_id, symbol, workers);
281 None
282 }
283 }
284 #[inline]
285 fn next_vector_row(
286 &mut self,
287 spec_version: u32,
288 ) -> Result<([f64; 3], [bool; 3], u64), crate::error::ParseError> {
289 if let Some(p) = self.peeked.take() {
290 return crate::parser::parse_vector_row_at(p.as_bytes(), 0, spec_version)
291 .map(|(row, _)| row);
292 }
293 let (row, pos) = crate::parser::parse_vector_row_at(self.bytes, self.pos, spec_version)?;
294 self.pos = pos;
295 Ok(row)
296 }
297 fn remainder_is_whitespace(&mut self) -> bool {
298 if self.utf8_bad {
299 return false;
300 }
301 if let Some(line) = self.peek_line()
302 && !line.trim().is_empty()
303 {
304 return false;
305 }
306 if self.utf8_bad {
309 return false;
310 }
311 self.bytes[self.pos..]
312 .iter()
313 .all(|b| b.is_ascii_whitespace())
314 }
315 fn blank_line_opens_section(&mut self) -> bool {
316 let Some(peeked) = self.peeked else {
318 return false;
319 };
320 if !peeked.trim().is_empty() {
321 return false;
322 }
323 match probe_section(&self.bytes[self.pos..]) {
324 SectionProbe::Opens => true,
325 SectionProbe::Closed => false,
326 SectionProbe::InvalidUtf8 => {
329 self.utf8_bad = true;
330 false
331 }
332 }
333 }
334
335 fn blank_opens_velocity_block(&mut self) -> bool {
336 match self.peek_line() {
337 Some(line) if line.trim().is_empty() => self.blank_line_opens_section(),
338 _ => true,
339 }
340 }
341}
342
343fn split_raw_line(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
344 if bytes.is_empty() {
345 return None;
346 }
347 match memchr::memchr(b'\n', bytes) {
348 Some(i) => Some((&bytes[..i], &bytes[i + 1..])),
349 None => Some((bytes, &[])),
350 }
351}
352
353fn strip_cr(line: &[u8]) -> &[u8] {
354 if line.last() == Some(&b'\r') {
355 &line[..line.len() - 1]
356 } else {
357 line
358 }
359}
360
361enum SectionProbe {
362 Opens,
363 Closed,
364 InvalidUtf8,
365}
366
367fn probe_section(after_blank: &[u8]) -> SectionProbe {
370 let Some((raw1, after1)) = split_raw_line(after_blank) else {
371 return SectionProbe::Closed;
372 };
373 let line1 = strip_cr(raw1);
374 if line1.iter().all(|b| matches!(b, b' ' | b'\t')) {
375 return SectionProbe::Closed;
376 }
377 let Ok(text1) = std::str::from_utf8(line1) else {
378 return SectionProbe::InvalidUtf8;
379 };
380 if text1.trim_start().starts_with('{') {
381 return SectionProbe::Closed;
382 }
383 let Some((raw2, _)) = split_raw_line(after1) else {
384 return SectionProbe::Closed;
385 };
386 let Ok(text2) = std::str::from_utf8(strip_cr(raw2)) else {
387 return SectionProbe::InvalidUtf8;
388 };
389 if crate::parser::line_has_component_label(text2) {
390 SectionProbe::Opens
391 } else {
392 SectionProbe::Closed
393 }
394}
395
396fn blank_opens_section(after_blank: &[u8]) -> bool {
397 matches!(probe_section(after_blank), SectionProbe::Opens)
398}
399
400pub(crate) fn no_frame_at(bytes: &[u8], pos: usize) -> bool {
404 match bytes.get(pos) {
405 None => true,
406 Some(b) if b.is_ascii_graphic() => false,
408 Some(_) => MemchrLines::from_bytes(&bytes[pos..]).remainder_is_whitespace(),
409 }
410}
411
412fn is_blank_line(line: &[u8]) -> bool {
416 match line.first() {
417 Some(b) if b.is_ascii_graphic() => false,
418 _ => std::str::from_utf8(line).is_ok_and(|s| s.trim().is_empty()),
419 }
420}
421
422pub struct ConFrameIterator<'a> {
433 pub(crate) lines: MemchrLines<'a>,
434 frame_index: u64,
438}
439
440#[cfg(target_arch = "x86_64")]
448#[target_feature(enable = "avx2,popcnt")]
449#[inline]
450unsafe fn skip_newlines_avx2(hay: &[u8], n: usize) -> (usize, usize) {
451 use std::arch::x86_64::{
452 __m256i, _mm256_cmpeq_epi8, _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_set1_epi8,
453 _popcnt32,
454 };
455 if n == 0 {
456 return (0, 0);
457 }
458 let mut need = n;
459 let mut offset = 0usize;
460 let ptr = hay.as_ptr();
461 let len = hay.len();
462 let nl = _mm256_set1_epi8(b'\n' as i8);
463 while offset + 32 <= len {
464 let chunk = unsafe { _mm256_loadu_si256(ptr.add(offset) as *const __m256i) };
466 let eq = _mm256_cmpeq_epi8(chunk, nl);
467 let mask = _mm256_movemask_epi8(eq) as u32;
468 let count = _popcnt32(mask as i32) as usize;
469 if count >= need {
470 let mut bits = mask;
471 for _ in 1..need {
472 bits &= bits - 1;
473 }
474 let bit = bits.trailing_zeros() as usize;
475 return (n, offset + bit + 1);
476 }
477 need -= count;
478 offset += 32;
479 }
480 while offset < len {
481 if hay[offset] == b'\n' {
482 need -= 1;
483 if need == 0 {
484 return (n, offset + 1);
485 }
486 }
487 offset += 1;
488 }
489 (n - need, len)
490}
491
492impl<'a> ConFrameIterator<'a> {
493 pub fn new(file_contents: &'a str) -> Self {
499 ConFrameIterator {
500 lines: MemchrLines::new(file_contents),
501 frame_index: 0,
502 }
503 }
504
505 pub(crate) fn starting_at(file_contents: &'a str, pos: usize) -> Self {
510 debug_assert!(pos == 0 || file_contents.as_bytes()[pos - 1] == b'\n');
511 let mut it = Self::new(file_contents);
512 it.lines.pos = pos;
513 it
514 }
515
516 pub fn from_bytes(bytes: &'a [u8]) -> Self {
522 ConFrameIterator {
523 lines: MemchrLines::from_bytes(bytes),
524 frame_index: 0,
525 }
526 }
527
528 pub(crate) fn frame_index(&self) -> u64 {
533 self.frame_index
534 }
535
536 fn note_skipped_frame(&mut self) -> Option<Result<(), error::ParseError>> {
537 self.frame_index = self.frame_index.saturating_add(1);
538 Some(Ok(()))
539 }
540
541 #[inline(always)]
551 fn advance_lines(&mut self, n: usize) -> Result<(), error::ParseError> {
552 self.lines.clear_peek();
553 if self.lines.newlines.is_some() {
554 return self.advance_lines_indexed(n);
555 }
556 #[cfg(target_arch = "x86_64")]
557 {
558 if n > 1 && std::is_x86_feature_detected!("avx2") {
559 return self.advance_lines_avx2(n);
560 }
561 }
562 self.advance_lines_memchr(n)
563 }
564
565 fn advance_lines_memchr(&mut self, n: usize) -> Result<(), error::ParseError> {
566 for i in 0..n {
567 let rest = &self.lines.bytes[self.lines.pos..];
568 match memchr::memchr(b'\n', rest) {
569 Some(pos) => {
570 self.lines.pos += pos + 1;
571 self.lines.line_no = self.lines.line_no.saturating_add(1);
572 }
573 None => {
574 if rest.is_empty() {
575 return Err(error::ParseError::IncompleteFrame);
576 }
577 self.lines.pos = self.lines.bytes.len();
578 self.lines.line_no = self.lines.line_no.saturating_add(1);
582 if i + 1 == n {
583 return Ok(());
584 }
585 return Err(error::ParseError::IncompleteFrame);
586 }
587 }
588 }
589 Ok(())
590 }
591
592 #[cfg(target_arch = "x86_64")]
598 #[inline]
599 fn advance_lines_avx2(&mut self, n: usize) -> Result<(), error::ParseError> {
600 let rest = &self.lines.bytes[self.lines.pos..];
601 let (found, end) = unsafe { skip_newlines_avx2(rest, n) };
602 if found == n {
603 self.lines.pos += end;
604 self.lines.line_no = self
607 .lines
608 .line_no
609 .saturating_add(u32::try_from(n).unwrap_or(u32::MAX));
610 return Ok(());
611 }
612 if rest.is_empty() {
613 return Err(error::ParseError::IncompleteFrame);
614 }
615 let has_tail = rest.last().copied() != Some(b'\n');
616 self.lines.pos = self.lines.bytes.len();
617 let counted = found + usize::from(has_tail);
618 self.lines.line_no = self
619 .lines
620 .line_no
621 .saturating_add(u32::try_from(counted).unwrap_or(u32::MAX));
622 if counted >= n {
623 Ok(())
624 } else {
625 Err(error::ParseError::IncompleteFrame)
626 }
627 }
628
629 fn advance_lines_to(&mut self, n: usize, end: Option<usize>) -> Result<(), error::ParseError> {
635 if self.lines.newlines.is_some() {
638 return self.advance_lines(n);
639 }
640 self.lines.clear_peek();
641 let pos = self.lines.pos;
642 let bytes = self.lines.bytes;
643 if let Some(end) = end
644 && pos < end
645 && end <= bytes.len()
646 && bytes[end - 1] == b'\n'
647 && memchr::memchr_iter(b'\n', &bytes[pos..end]).count() == n
648 {
649 self.lines.pos = end;
650 self.lines.line_no = self
653 .lines
654 .line_no
655 .saturating_add(u32::try_from(n).unwrap_or(u32::MAX));
656 return Ok(());
657 }
658 self.advance_lines(n)
659 }
660
661 fn advance_lines_indexed(&mut self, n: usize) -> Result<(), error::ParseError> {
666 let len = self.lines.bytes.len();
667 let step = {
668 let nl = self.lines.newlines.as_deref().unwrap();
669 let nlines = line_count(nl, len);
670 let line_no = if self.lines.pos == line_start_pos(nl, len, self.lines.index_line) {
671 self.lines.index_line
672 } else {
673 line_no_for_pos(nl, len, self.lines.pos)
674 };
675 if n == 0 {
676 IndexStep::Stay(line_no)
677 } else if line_no >= nlines {
678 IndexStep::Missing(line_no)
679 } else {
680 match line_no.checked_add(n) {
681 Some(target) if target <= nlines => {
682 let pos = if target == nlines {
683 len
684 } else {
685 line_start_pos(nl, len, target)
686 };
687 IndexStep::Landed(target, pos)
688 }
689 _ => IndexStep::Short(nlines, len),
690 }
691 }
692 };
693 match step {
694 IndexStep::Stay(line_no) => {
695 self.lines.index_line = line_no;
696 self.lines.line_no = u32::try_from(line_no).unwrap_or(u32::MAX);
697 Ok(())
698 }
699 IndexStep::Missing(line_no) => {
700 self.lines.index_line = line_no;
701 self.lines.line_no = u32::try_from(line_no).unwrap_or(u32::MAX);
702 Err(error::ParseError::IncompleteFrame)
703 }
704 IndexStep::Landed(line_no, pos) => {
705 self.lines.index_line = line_no;
706 self.lines.line_no = u32::try_from(line_no).unwrap_or(u32::MAX);
707 self.lines.pos = pos;
708 Ok(())
709 }
710 IndexStep::Short(line_no, pos) => {
711 self.lines.index_line = line_no;
712 self.lines.line_no = u32::try_from(line_no).unwrap_or(u32::MAX);
713 self.lines.pos = pos;
714 Err(error::ParseError::IncompleteFrame)
715 }
716 }
717 }
718
719 fn read_line_str(&mut self) -> Option<&'a str> {
721 self.lines.clear_peek();
722 self.lines.next_line()
723 }
724
725 pub fn forward_fast(&mut self) -> Option<Result<(), error::ParseError>> {
729 self.forward_fast_to(None)
730 }
731
732 #[inline(always)]
737 pub(crate) fn forward_fast_to(
738 &mut self,
739 end: Option<usize>,
740 ) -> Option<Result<(), error::ParseError>> {
741 self.lines.clear_peek();
742 if self.lines.pos >= self.lines.bytes.len() {
743 return None;
744 }
745 if let Err(e) = self.advance_lines(6) {
747 return Some(Err(e));
748 }
749 let natm_types: usize = match self.read_line_str() {
751 Some(line) => match crate::parser::parse_count_line(line, 1) {
752 Ok(v) => v[0],
753 Err(e) => return Some(Err(e)),
754 },
755 None => return Some(Err(error::ParseError::IncompleteHeader)),
756 };
757 if natm_types == 0 {
758 return match crate::parser::read_zero_atom_tail(|| self.read_line_str()) {
761 Ok(()) => self.note_skipped_frame(),
762 Err(e) => Some(Err(e)),
763 };
764 }
765 let natms_per_type: Vec<usize> = match self.read_line_str() {
767 Some(line) => match crate::parser::parse_count_line(line, natm_types) {
768 Ok(v) => v,
769 Err(e) => return Some(Err(e)),
770 },
771 None => return Some(Err(error::ParseError::IncompleteHeader)),
772 };
773 if natms_per_type.contains(&0) {
774 return Some(Err(error::ParseError::InvalidCount(
775 "each per-type count must be >= 1".into(),
776 )));
777 }
778 if let Err(e) = self.advance_lines(1) {
780 return Some(Err(e));
781 }
782 let total_atoms = natms_per_type
785 .iter()
786 .fold(0usize, |sum, &n| sum.saturating_add(n));
787 let coord_block_lines = total_atoms.saturating_add(natm_types.saturating_mul(2));
788 if let Err(e) = self.advance_lines_to(coord_block_lines, end) {
789 return Some(Err(e));
790 }
791 self.lines.clear_peek();
793 loop {
794 let rest = &self.lines.bytes[self.lines.pos..];
795 if rest.is_empty() {
796 break;
797 }
798 let next_eol = memchr::memchr(b'\n', rest);
799 let line = match next_eol {
800 Some(pos) => &rest[..pos],
801 None => rest,
802 };
803 if !is_blank_line(line) {
806 break;
807 }
808 let after = match next_eol {
809 Some(pos) => &rest[pos + 1..],
810 None => &[][..],
811 };
812 if after.iter().all(|b| b.is_ascii_whitespace()) {
815 self.lines.pos = self.lines.bytes.len();
816 break;
817 }
818 if !blank_opens_section(after) {
821 break;
822 }
823 self.lines.pos += next_eol.map(|p| p + 1).unwrap_or(rest.len());
827 self.lines.line_no = self.lines.line_no.saturating_add(1);
830 if let Err(e) = self.advance_lines_to(coord_block_lines, end) {
831 return Some(Err(e));
832 }
833 }
834 self.note_skipped_frame()
835 }
836
837 pub fn forward(&mut self) -> Option<Result<(), error::ParseError>> {
849 self.forward_fast()
851 }
852
853 pub fn byte_pos(&self) -> usize {
855 self.lines.pos
856 }
857
858 pub fn skip_frames(&mut self, n: usize) -> Result<usize, error::ParseError> {
861 self.skip_frames_counted(n).map_err(|(_, err)| err)
862 }
863
864 pub(crate) fn skip_frames_counted(
868 &mut self,
869 n: usize,
870 ) -> Result<usize, (usize, error::ParseError)> {
871 let mut skipped = 0usize;
872 for _ in 0..n {
873 match self.forward() {
874 None => break,
875 Some(Err(e)) => return Err((skipped, e)),
876 Some(Ok(())) => skipped += 1,
877 }
878 }
879 Ok(skipped)
880 }
881
882 pub fn next_with_raw_span(
895 &mut self,
896 file_contents: &'a str,
897 ) -> Option<Result<(types::ConFrame, &'a str), error::ParseError>> {
898 let base = file_contents.as_ptr() as usize;
899 let start = {
900 let line = self.lines.peek_line()?;
901 line.as_ptr() as usize - base
902 };
903 let frame = match self.next()? {
904 Ok(f) => f,
905 Err(e) => return Some(Err(e)),
906 };
907 let end = match self.lines.peek_line() {
908 Some(line) => line.as_ptr() as usize - base,
909 None => file_contents.len(),
910 };
911 debug_assert!(end >= start && end <= file_contents.len());
912 Some(Ok((frame, &file_contents[start..end])))
913 }
914
915 pub(crate) fn next_with_end(
917 &mut self,
918 ) -> Option<Result<(types::ConFrame, usize), error::ParseError>> {
919 let frame = match self.next()? {
920 Ok(f) => f,
921 Err(e) => return Some(Err(e)),
922 };
923 let end = match self.lines.peek_line() {
924 Some(line) => line.as_ptr() as usize - self.lines.bytes.as_ptr() as usize,
925 None => self.lines.pos,
926 };
927 Some(Ok((frame, end)))
928 }
929}
930
931impl<'a> Iterator for ConFrameIterator<'a> {
932 type Item = Result<types::ConFrame, error::ParseError>;
937
938 fn next(&mut self) -> Option<Self::Item> {
945 if let Some(err) = self.lines.utf8_error() {
948 return Some(Err(err));
949 }
950 if self.lines.remainder_is_whitespace() {
954 if let Some(err) = self.lines.utf8_error() {
955 return Some(Err(err));
956 }
957 while self.lines.next_line().is_some() {}
958 return None;
959 }
960 if let Some(err) = self.lines.utf8_error() {
961 return Some(Err(err));
962 }
963 let max_rows = Some(self.lines.unread_len() / crate::parser::MIN_COORD_ROW_BYTES);
965 let mut frame = match crate::parser::parse_single_frame_ls(&mut self.lines, max_rows) {
966 Ok(f) => f,
967 Err(e) => return Some(Err(self.lines.prefer_utf8(e))),
968 };
969 let sections = match parse_declared_sections(&mut self.lines, &mut frame) {
973 Ok(n) => n,
974 Err(e) => return Some(Err(self.lines.prefer_utf8(e))),
977 };
978 if sections > 0 {
979 frame.sequential_plain = false;
983 frame.install_declared_section_soa();
984 }
985 self.frame_index = self.frame_index.saturating_add(1);
986 Some(Ok(frame))
987 }
988}
989
990#[cfg(test)]
991mod aos_soa_agreement_tests {
992 use super::*;
993 use std::path::PathBuf;
994
995 #[test]
996 fn iterator_vel_forces_soa_matches_aos() {
997 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
998 .join("resources/test/tiny_cuh2_vel_forces.con");
999 let text = std::fs::read_to_string(&p).expect("fixture");
1000 let fr = ConFrameIterator::new(&text)
1001 .next()
1002 .expect("frame")
1003 .expect("parse");
1004 let n = fr.atom_data.len();
1005 assert!(n > 0);
1006 assert_eq!(fr.positions.nrows(), n);
1007 let has_vel = fr.atom_data.iter().any(|a| a.velocity().is_some());
1008 let has_frc = fr.atom_data.iter().any(|a| a.force().is_some());
1009 if has_vel {
1010 assert_eq!(
1011 fr.velocities.nrows(),
1012 n,
1013 "SoA velocities must match AoS after section parse"
1014 );
1015 }
1016 if has_frc {
1017 assert_eq!(fr.forces.nrows(), n, "SoA forces must match AoS");
1018 }
1019 for (i, a) in fr.atom_data.iter().enumerate() {
1020 let p = fr.positions.as_f64_row(i);
1021 assert_eq!([a.x(), a.y(), a.z()], p);
1022 if let Some(v) = a.velocity() {
1023 assert_eq!(v, fr.velocities.as_f64_row(i));
1024 }
1025 if let Some(f) = a.force() {
1026 assert_eq!(f, fr.forces.as_f64_row(i));
1027 }
1028 }
1029 }
1030
1031 #[test]
1034 fn sync_skips_pos_when_nrows_matches_keeps_force_soa() {
1035 let p =
1036 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2_forces.con");
1037 let text = std::fs::read_to_string(&p).expect("fixture");
1038 let fr = ConFrameIterator::new(&text)
1039 .next()
1040 .expect("frame")
1041 .expect("parse");
1042 let n = fr.atom_data.len();
1043 assert_eq!(fr.positions.nrows(), n);
1044 assert_eq!(fr.forces.nrows(), n);
1045 let p0 = fr.positions.as_f64_row(0);
1048 let mut fr2 = fr.clone();
1049 fr2.sync_arrays_from_atom_data();
1050 assert_eq!(fr2.positions.as_f64_row(0), p0);
1051 assert_eq!(fr2.forces.nrows(), n);
1052 assert_eq!(
1053 fr2.forces.as_f64_row(0),
1054 fr2.atom_data[0].force().expect("force")
1055 );
1056 }
1057
1058 #[test]
1061 fn declared_sections_soa_matches_every_atom() {
1062 let n = 400;
1063 let mut text = String::new();
1064 text.push_str("soa fill\n");
1065 text.push_str(
1066 "{\"con_spec_version\":2,\"sections\":[\"velocities\",\"forces\",\"energies\"]}\n",
1067 );
1068 text.push_str("10.0 10.0 10.0\n90.0 90.0 90.0\n0 0\n0 0 0\n1\n");
1069 text.push_str(&format!("{n}\n63.546\nCu\nCoordinates of Component 1\n"));
1070 for i in 0..n {
1071 let x = (i as f64) * 0.125 - 3.5;
1072 let y = (i as f64) * -0.05 + 1.0;
1073 let z = (i % 17) as f64 * 0.333;
1074 text.push_str(&format!("{x:.8} {y:.8} {z:.8} 0 {i}\n"));
1075 }
1076 text.push_str("\nCu\nVelocities of Component 1\n");
1077 for i in 0..n {
1078 let vx = (i as f64) * 1.0e-4 - 0.02;
1079 let vy = ((i * 3) % 11) as f64 * -1.0e-3;
1080 let vz = (i as f64).sin() * 0.01;
1081 text.push_str(&format!("{vx:.8} {vy:.8} {vz:.8} 0 {i}\n"));
1082 }
1083 text.push_str("\nCu\nForces of Component 1\n");
1084 for i in 0..n {
1085 let fx = (i as f64) * 0.01;
1086 let fy = -fx / 2.0;
1087 let fz = (i % 5) as f64;
1088 text.push_str(&format!("{fx:.8} {fy:.8} {fz:.8} 0 {i}\n"));
1089 }
1090 text.push_str("\nCu\nEnergies of Component 1\n");
1091 for i in 0..n {
1092 let e = (i as f64) * 0.02 - 4.0;
1093 text.push_str(&format!("{e:.8} 0 {i}\n"));
1094 }
1095 let frame = ConFrameIterator::new(&text)
1096 .next()
1097 .expect("frame")
1098 .expect("parse");
1099 assert_eq!(frame.atom_data.len(), n);
1100 assert_eq!(frame.velocities.nrows(), n);
1101 assert_eq!(frame.forces.nrows(), n);
1102 assert_eq!(frame.atom_energies.len(), n);
1103 assert_eq!(frame.positions.nrows(), n);
1104 assert!(frame.magmoms.nrows() == 0);
1105 for i in 0..n {
1106 let atom = &frame.atom_data[i];
1107 let pos = frame.positions.as_f64_row(i);
1108 assert_eq!(pos[0].to_bits(), atom.x().to_bits());
1109 assert_eq!(pos[1].to_bits(), atom.y().to_bits());
1110 assert_eq!(pos[2].to_bits(), atom.z().to_bits());
1111 let vel = atom.velocity().unwrap();
1112 let soa_v = frame.velocities.as_f64_row(i);
1113 assert_eq!(soa_v[0].to_bits(), vel[0].to_bits());
1114 assert_eq!(soa_v[1].to_bits(), vel[1].to_bits());
1115 assert_eq!(soa_v[2].to_bits(), vel[2].to_bits());
1116 let frc = atom.force().unwrap();
1117 let soa_f = frame.forces.as_f64_row(i);
1118 assert_eq!(soa_f[0].to_bits(), frc[0].to_bits());
1119 assert_eq!(soa_f[1].to_bits(), frc[1].to_bits());
1120 assert_eq!(soa_f[2].to_bits(), frc[2].to_bits());
1121 assert_eq!(
1122 frame.atom_energies.get_f64(i).to_bits(),
1123 atom.energy().unwrap().to_bits()
1124 );
1125 assert_eq!(frame.atom_ids[i], atom.atom_id);
1126 }
1127 }
1128}
1129
1130pub fn read_all_frames(path: &Path) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
1137 read_all_frames_with_threads(path, None)
1138}
1139
1140pub fn frames_from_text(
1156 text: &str,
1157 num_threads: Option<usize>,
1158) -> Result<Vec<types::ConFrame>, error::ParseError> {
1159 #[cfg(feature = "parallel")]
1160 {
1161 if let Some(n) = num_threads.filter(|n| *n >= 2) {
1162 return Ok(frames_from_bytes_phased(text.as_bytes(), Some(n))?.frames);
1166 }
1167 }
1168 #[cfg(not(feature = "parallel"))]
1169 {
1170 let _ = num_threads;
1171 }
1172 ConFrameIterator::new(text).collect()
1173}
1174
1175pub fn read_all_frames_with_threads(
1187 path: &Path,
1188 num_threads: Option<usize>,
1189) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
1190 if matches!(num_threads, Some(n) if n >= 2) {
1191 let bytes = crate::compression::read_bytes_owned(path)?;
1192 return Ok(frames_from_bytes_located(bytes.as_slice(), num_threads)?);
1193 }
1194 stream_frames(path).map_err(public_load_error)
1195}
1196
1197fn public_load_error(err: LoadError) -> Box<dyn std::error::Error> {
1198 match err {
1199 LoadError::Parse(located) => Box::new(located),
1200 other => Box::new(other),
1201 }
1202}
1203
1204fn stream_frames(path: &Path) -> Result<Vec<types::ConFrame>, LoadError> {
1208 use std::io::Read;
1209 let mut file = std::fs::File::open(path).map_err(LoadError::Io)?;
1210 let mut magic = [0u8; 4];
1211 let n = file.read(&mut magic).map_err(LoadError::Io)?;
1212 let compression = crate::compression::detect_compression(&magic[..n]);
1213 if matches!(compression, crate::compression::Compression::None) {
1214 let file_len = path.metadata().map(|m| m.len()).unwrap_or(0);
1215 if file_len > 0 && file_len <= 128 * 1024 * 1024 {
1222 drop(file);
1223 let source = crate::compression::read_bytes_owned(path).map_err(load_error_from_box)?;
1224 return frames_from_bytes_located(source.as_slice(), Some(1)).map_err(LoadError::Parse);
1225 }
1226 return stream_reader(&mut file, &magic[..n], file_len);
1227 }
1228 drop(file);
1229 let bytes = crate::compression::read_file_bytes(path).map_err(load_error_from_box)?;
1232 let budget = crate::compression::decode_thread_budget();
1233 let threads = if budget >= 2 { Some(budget) } else { Some(1) };
1234 frames_from_bytes_located(&bytes, threads).map_err(LoadError::Parse)
1235}
1236
1237fn utf8_text(bytes: &[u8], eof: bool) -> Result<&str, LoadError> {
1240 match std::str::from_utf8(bytes) {
1241 Ok(text) => Ok(text),
1242 Err(err) if eof => earlier_parse_or_utf8(bytes, err),
1243 Err(err) => Err(LoadError::Utf8(err)),
1244 }
1245}
1246
1247fn error_needs_more_input(error: &error::ParseError) -> bool {
1248 matches!(
1249 error,
1250 error::ParseError::IncompleteHeader
1251 | error::ParseError::IncompleteFrame
1252 | error::ParseError::IncompleteVelocitySection
1253 | error::ParseError::IncompleteForceSection
1254 | error::ParseError::IncompleteEnergySection
1255 | error::ParseError::IncompleteSection(_)
1256 )
1257}
1258
1259fn earlier_parse_or_utf8(bytes: &[u8], err: std::str::Utf8Error) -> Result<&str, LoadError> {
1260 let valid = err.valid_up_to();
1261 if valid > 0 {
1262 if let Ok(prefix) = std::str::from_utf8(&bytes[..valid]) {
1263 if let Err(located) = frames_from_text_located(prefix, Some(1)) {
1264 if !error_needs_more_input(&located.error) {
1267 return Err(LoadError::Parse(located));
1268 }
1269 }
1270 }
1271 }
1272 Err(LoadError::Utf8(err))
1273}
1274
1275fn stream_reader(
1276 reader: &mut dyn std::io::Read,
1277 prefix: &[u8],
1278 file_len: u64,
1279) -> Result<Vec<types::ConFrame>, LoadError> {
1280 let mut buf = Vec::with_capacity(8 << 20);
1281 buf.extend_from_slice(prefix);
1282 let mut frames = Vec::new();
1283 let mut sized = false;
1284 let mut line_base = 0u32;
1285 let mut eof = false;
1286 let mut read_more = |buf: &mut Vec<u8>, eof: &mut bool| -> Result<(), LoadError> {
1287 if *eof {
1288 return Ok(());
1289 }
1290 let start = buf.len();
1291 buf.resize(start + (8 << 20), 0);
1292 let got = reader.read(&mut buf[start..]).map_err(LoadError::Io)?;
1293 buf.truncate(start + got);
1294 if got == 0 {
1295 *eof = true;
1296 }
1297 Ok(())
1298 };
1299 if buf.len() < (8 << 20) {
1300 read_more(&mut buf, &mut eof)?;
1301 }
1302 loop {
1303 if buf.is_empty() {
1304 break;
1305 }
1306 let end = if eof {
1307 buf.len()
1308 } else {
1309 match buf.iter().rposition(|byte| *byte == b'\n') {
1310 Some(i) => i + 1,
1311 None => {
1312 read_more(&mut buf, &mut eof)?;
1313 continue;
1314 }
1315 }
1316 };
1317 let text = utf8_text(&buf[..end], eof)?;
1318 let (parsed, consumed) = take_complete_frames(text, eof, line_base, frames.len() as u64)?;
1319 if consumed == 0 && !eof {
1320 if frames.is_empty() && file_len > 0 && file_len <= 512 * 1024 * 1024 {
1324 while !eof {
1325 let before = buf.len();
1326 read_more(&mut buf, &mut eof)?;
1327 if buf.len() == before {
1328 break;
1329 }
1330 }
1331 continue;
1332 }
1333 let before = buf.len();
1334 read_more(&mut buf, &mut eof)?;
1335 if buf.len() == before && !eof {
1336 eof = true;
1337 }
1338 continue;
1339 }
1340 frames.extend(parsed);
1341 if !sized && consumed > 0 && !frames.is_empty() && file_len > 0 {
1342 let per = (consumed / frames.len()).max(1);
1343 let guess = (file_len as usize) / per + 8;
1344 if guess > frames.capacity() {
1345 frames.reserve_exact(guess - frames.len());
1346 }
1347 sized = true;
1348 }
1349 line_base += buf[..consumed]
1350 .iter()
1351 .filter(|byte| **byte == b'\n')
1352 .count() as u32;
1353 buf.drain(..consumed);
1354 if buf.is_empty() && eof {
1355 break;
1356 }
1357 if !eof && buf.len() < (8 << 20) {
1358 read_more(&mut buf, &mut eof)?;
1359 }
1360 }
1361 Ok(frames)
1362}
1363
1364fn take_complete_frames(
1365 text: &str,
1366 eof: bool,
1367 line_base: u32,
1368 frame_base: u64,
1369) -> Result<(Vec<types::ConFrame>, usize), LoadError> {
1370 let mut iter = ConFrameIterator::new(text);
1371 iter.lines.line_no = line_base;
1372 let mut frames = Vec::new();
1373 loop {
1374 iter.lines.clear_peek();
1377 let start = iter.lines.pos;
1378 if !eof && text[start..].trim().is_empty() {
1379 return Ok((frames, start));
1382 }
1383 match iter.next() {
1384 Some(Ok(frame)) => frames.push(frame),
1385 Some(Err(error)) => {
1386 if eof || !error_needs_more_input(&error) {
1390 return Err(LoadError::Parse(error::LocatedParseError::new(
1391 error,
1392 iter.lines.line_no(),
1393 frame_base + frames.len() as u64,
1394 )));
1395 }
1396 return Ok((frames, start));
1397 }
1398 None => {
1399 if eof {
1400 return Ok((frames, text.len()));
1401 }
1402 return Ok((frames, start));
1403 }
1404 }
1405 }
1406}
1407
1408#[derive(Debug)]
1410pub struct PositionBlock {
1425 pub n_frames: usize,
1426 pub n_atoms: usize,
1427 pub values: Vec<f64>,
1428}
1429
1430pub fn read_positions_with_threads(
1453 path: &Path,
1454 num_threads: Option<usize>,
1455) -> Result<PositionBlock, Box<dyn std::error::Error>> {
1456 let contents = crate::compression::read_file_contents(path)?;
1457 let block = match &contents {
1458 crate::compression::FileContents::Owned(text) => {
1459 fill_position_block(text.as_bytes(), num_threads)
1460 }
1461 crate::compression::FileContents::Mapped(map) => fill_position_block(map, num_threads),
1462 }?;
1463 Ok(block)
1464}
1465
1466pub struct CoordColumns {
1471 pub block: PositionBlock,
1472 pub masses: Vec<f64>,
1473 pub cells: Vec<f64>,
1474 pub atom_ids: Vec<u64>,
1476 pub fixed: Vec<u8>,
1478 pub species: Vec<Vec<(String, usize)>>,
1483 pub has_section: bool,
1484 pub partial_fixed: bool,
1486}
1487
1488pub fn read_coord_columns(
1493 path: &Path,
1494 num_threads: Option<usize>,
1495) -> Result<CoordColumns, Box<dyn std::error::Error>> {
1496 let contents = crate::compression::read_file_contents(path)?;
1497 let bytes = contents.as_bytes();
1498 if let Ok(Some(columns)) = certified_columns(bytes, num_threads, true) {
1499 return Ok(columns);
1500 }
1501 let frames = frames_from_bytes_located(bytes, num_threads)?;
1502 Ok(columns_from_frames(&frames))
1503}
1504
1505fn certified_columns(
1506 bytes: &[u8],
1507 num_threads: Option<usize>,
1508 identity: bool,
1509) -> Result<Option<CoordColumns>, error::LocatedParseError> {
1510 let mut parsed = {
1511 let parsed = if parallel_column_threads(num_threads) {
1512 #[cfg(feature = "parallel")]
1513 {
1514 crate::columns::positions_parallel(bytes, num_threads.unwrap_or(1), identity)
1515 }
1516 #[cfg(not(feature = "parallel"))]
1517 {
1518 let _ = num_threads;
1519 if identity {
1520 crate::columns::positions_with_identity(bytes)
1521 } else {
1522 crate::columns::positions_from_con_bytes(bytes)
1523 }
1524 }
1525 } else if identity {
1526 crate::columns::positions_with_identity(bytes)
1527 } else {
1528 crate::columns::positions_from_con_bytes(bytes)
1529 };
1530 match parsed {
1531 Ok(parsed) if parsed.stock => parsed,
1532 _ => return Ok(None),
1533 }
1534 };
1535 if parsed.n_frames == 0 {
1536 return Ok(Some(CoordColumns {
1537 block: PositionBlock {
1538 n_frames: 0,
1539 n_atoms: 0,
1540 values: Vec::new(),
1541 },
1542 masses: Vec::new(),
1543 cells: Vec::new(),
1544 atom_ids: Vec::new(),
1545 fixed: Vec::new(),
1546 species: Vec::new(),
1547 has_section: false,
1548 partial_fixed: false,
1549 }));
1550 }
1551 let Some(n_atoms) = parsed.rectangular_atoms() else {
1552 let first = parsed.counts[0];
1553 let index = parsed
1554 .counts
1555 .iter()
1556 .position(|count| *count != first)
1557 .unwrap_or(0);
1558 let line = file_line_at_bytes(
1559 bytes,
1560 parsed.spans.get(index).map(|span| span.start).unwrap_or(0),
1561 );
1562 return Err(error::LocatedParseError::new(
1563 error::ParseError::InvalidCount(
1564 "frame atom count does not match the first frame".into(),
1565 ),
1566 line,
1567 index as u64,
1568 ));
1569 };
1570 let ColumnIdentity {
1571 atom_ids,
1572 fixed,
1573 species,
1574 partial_fixed,
1575 } = if identity {
1576 match frame_identity(&parsed, n_atoms) {
1577 Some(identity) => identity,
1578 None => return Ok(None),
1579 }
1580 } else {
1581 ColumnIdentity::default()
1582 };
1583 let has_section = parsed.has_section;
1584 let values = std::mem::take(&mut parsed.positions);
1585 Ok(Some(CoordColumns {
1586 block: PositionBlock {
1587 n_frames: parsed.n_frames,
1588 n_atoms,
1589 values,
1590 },
1591 masses: parsed.masses,
1592 cells: parsed.cells,
1593 atom_ids,
1594 fixed,
1595 species,
1596 has_section,
1597 partial_fixed,
1598 }))
1599}
1600
1601#[derive(Default)]
1603struct ColumnIdentity {
1604 atom_ids: Vec<u64>,
1605 fixed: Vec<u8>,
1606 species: Vec<Vec<(String, usize)>>,
1607 partial_fixed: bool,
1608}
1609
1610fn frame_identity(parsed: &crate::columns::ConPositions, n_atoms: usize) -> Option<ColumnIdentity> {
1613 let n = parsed.n_frames.saturating_mul(n_atoms);
1614 if parsed.ids.len() != n || parsed.flags.len() != n {
1615 return None;
1616 }
1617 let mut runs = Vec::with_capacity(parsed.n_frames);
1618 let mut components = parsed.components.iter();
1619 for &count in &parsed.counts {
1620 let mut left = count;
1621 let mut frame_runs = Vec::new();
1622 while left > 0 {
1623 let (symbol, width) = components.next()?;
1624 if *width == 0 || *width > left {
1625 return None;
1626 }
1627 frame_runs.push((symbol.to_string(), *width));
1628 left -= *width;
1629 }
1630 runs.push(frame_runs);
1631 }
1632 if components.next().is_some() {
1633 return None;
1634 }
1635 let partial_fixed = parsed.flags.iter().any(|flag| *flag != 0 && *flag != 7);
1636 let species = if runs.windows(2).all(|pair| pair[0] == pair[1]) {
1637 runs.into_iter().take(1).collect()
1638 } else {
1639 runs
1640 };
1641 Some(ColumnIdentity {
1642 atom_ids: parsed.ids.clone(),
1643 fixed: parsed.flags.clone(),
1644 species,
1645 partial_fixed,
1646 })
1647}
1648
1649fn parallel_column_threads(num_threads: Option<usize>) -> bool {
1650 #[cfg(feature = "parallel")]
1653 {
1654 matches!(num_threads, Some(n) if n >= 2)
1655 }
1656 #[cfg(not(feature = "parallel"))]
1657 {
1658 let _ = num_threads;
1659 false
1660 }
1661}
1662
1663fn columns_from_frames(frames: &[types::ConFrame]) -> CoordColumns {
1664 let n_frames = frames.len();
1665 let n_atoms = frames
1666 .first()
1667 .map(|frame| frame.positions.nrows())
1668 .unwrap_or(0);
1669 let mut values = Vec::with_capacity(n_frames * n_atoms * 3);
1670 let mut masses = Vec::with_capacity(n_frames * n_atoms);
1671 let mut cells = Vec::with_capacity(n_frames * 6);
1672 let mut atom_ids = Vec::with_capacity(n_frames * n_atoms);
1673 let mut fixed = Vec::with_capacity(n_frames * n_atoms);
1674 let mut runs = Vec::with_capacity(n_frames);
1675 let mut has_section = false;
1676 let mut partial_fixed = false;
1677 for frame in frames {
1678 cells.extend_from_slice(&frame.header.boxl);
1679 cells.extend_from_slice(&frame.header.angles);
1680 if let Some(src) = frame.positions.f64_slice() {
1681 values.extend_from_slice(src);
1682 } else {
1683 for row in 0..frame.positions.nrows() {
1684 values.extend_from_slice(&frame.positions.as_f64_row(row));
1685 }
1686 }
1687 for (type_index, &count) in frame.header.natms_per_type.iter().enumerate() {
1688 let mass = frame
1689 .header
1690 .masses_per_type
1691 .get(type_index)
1692 .copied()
1693 .unwrap_or(0.0);
1694 masses.extend(std::iter::repeat_n(mass, count));
1695 }
1696 has_section |= frame.has_velocities()
1697 || frame.has_forces()
1698 || frame.has_energies()
1699 || frame.has_charges()
1700 || frame.has_spins()
1701 || frame.has_magmoms()
1702 || frame.has_displacements()
1703 || frame.has_spreads();
1704 let mut frame_runs: Vec<(String, usize)> = Vec::new();
1705 for atom in &frame.atom_data {
1706 atom_ids.push(atom.atom_id);
1707 let mask = crate::types::encode_fixed_bitmask(atom.fixed());
1708 if mask != 0 && mask != 7 {
1709 partial_fixed = true;
1710 }
1711 fixed.push(mask);
1712 if let Some((symbol, count)) = frame_runs.last_mut()
1713 && symbol == atom.symbol.as_ref()
1714 {
1715 *count += 1;
1716 continue;
1717 }
1718 frame_runs.push((atom.symbol.to_string(), 1));
1719 }
1720 runs.push(frame_runs);
1721 }
1722 let species = if runs.windows(2).all(|pair| pair[0] == pair[1]) {
1723 runs.into_iter().take(1).collect()
1724 } else {
1725 runs
1726 };
1727 CoordColumns {
1728 block: PositionBlock {
1729 n_frames,
1730 n_atoms,
1731 values,
1732 },
1733 masses,
1734 cells,
1735 atom_ids,
1736 fixed,
1737 species,
1738 has_section,
1739 partial_fixed,
1740 }
1741}
1742
1743fn file_line_at_bytes(bytes: &[u8], byte: usize) -> u32 {
1745 let end = byte.min(bytes.len());
1746 1 + memchr::memchr_iter(b'\n', &bytes[..end]).count() as u32
1747}
1748
1749fn share_cursor(local: u32, bytes: &[u8], share_start: usize) -> u32 {
1754 let base = file_line_at_bytes(bytes, share_start);
1755 if local == 0 {
1756 base
1757 } else {
1758 base.saturating_add(local.saturating_sub(1))
1759 }
1760}
1761
1762fn frame_starts(bytes: &[u8]) -> Result<Vec<usize>, error::LocatedParseError> {
1763 let mut scanner = ConFrameIterator::from_bytes(bytes);
1764 let mut boundaries = Vec::new();
1765 loop {
1766 scanner.lines.clear_peek();
1767 let start = scanner.lines.pos;
1768 if no_frame_at(bytes, start) {
1769 break;
1770 }
1771 boundaries.push(start);
1772 match scanner.forward_fast() {
1773 Some(Ok(())) => {}
1774 Some(Err(err)) => {
1775 return Err(error::LocatedParseError::new(
1776 err,
1777 scanner.lines.line_no(),
1778 scanner.frame_index(),
1779 ));
1780 }
1781 None => break,
1782 }
1783 }
1784 Ok(boundaries)
1785}
1786
1787fn parse_span_located(
1789 bytes: &[u8],
1790 start: usize,
1791 end: usize,
1792 frame_index: u64,
1793) -> Result<types::ConFrame, error::LocatedParseError> {
1794 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
1795 match iter.next() {
1796 Some(Ok(frame)) => Ok(frame),
1797 Some(Err(err)) => Err(error::LocatedParseError::new(
1798 err,
1799 share_cursor(iter.lines.line_no(), bytes, start),
1800 frame_index,
1801 )),
1802 None => Err(error::LocatedParseError::new(
1803 error::ParseError::IncompleteFrame,
1804 file_line_at_bytes(bytes, start),
1805 frame_index,
1806 )),
1807 }
1808}
1809
1810fn copy_positions(frame: &types::ConFrame, dest: &mut [f64]) -> Result<(), error::ParseError> {
1811 if frame.positions.nrows().saturating_mul(3) != dest.len() {
1812 return Err(error::ParseError::InvalidCount(
1813 "frame atom count does not match the first frame".into(),
1814 ));
1815 }
1816 if let Some(src) = frame.positions.f64_slice() {
1817 dest.copy_from_slice(src);
1818 return Ok(());
1819 }
1820 for (i, slot) in dest.as_chunks_mut::<3>().0.iter_mut().enumerate() {
1821 slot.copy_from_slice(&frame.positions.as_f64_row(i));
1822 }
1823 Ok(())
1824}
1825
1826fn fill_position_block(
1827 bytes: &[u8],
1828 num_threads: Option<usize>,
1829) -> Result<PositionBlock, error::LocatedParseError> {
1830 match certified_columns(bytes, num_threads, false) {
1831 Ok(Some(columns)) => return Ok(columns.block),
1832 Ok(None) => {}
1833 Err(err) => return Err(err),
1834 }
1835 let boundaries = frame_starts(bytes)?;
1836 if boundaries.is_empty() {
1837 return Ok(PositionBlock {
1838 n_frames: 0,
1839 n_atoms: 0,
1840 values: Vec::new(),
1841 });
1842 }
1843 let end0 = boundaries.get(1).copied().unwrap_or(bytes.len());
1844 let first = parse_span_located(bytes, boundaries[0], end0, 0)?;
1845 let n_atoms = first.positions.nrows();
1846 let n_frames = boundaries.len();
1847 let stride = n_atoms.saturating_mul(3);
1848 let mut values = vec![0.0_f64; n_frames.saturating_mul(stride)];
1849 if stride == 0 {
1850 return Ok(PositionBlock {
1851 n_frames,
1852 n_atoms,
1853 values,
1854 });
1855 }
1856 fill_position_spans(bytes, &boundaries, n_atoms, num_threads, &mut values)?;
1857 Ok(PositionBlock {
1858 n_frames,
1859 n_atoms,
1860 values,
1861 })
1862}
1863
1864fn fill_position_spans(
1865 bytes: &[u8],
1866 boundaries: &[usize],
1867 n_atoms: usize,
1868 num_threads: Option<usize>,
1869 values: &mut [f64],
1870) -> Result<(), error::LocatedParseError> {
1871 let stride = n_atoms * 3;
1872 let write_one = |i: usize, dest: &mut [f64]| {
1873 let start = boundaries[i];
1874 let end = boundaries.get(i + 1).copied().unwrap_or(bytes.len());
1875 let frame = parse_span_located(bytes, start, end, i as u64)?;
1876 copy_positions(&frame, dest).map_err(|error| {
1877 error::LocatedParseError::new(error, file_line_at_bytes(bytes, start), i as u64)
1878 })
1879 };
1880 #[cfg(feature = "parallel")]
1881 {
1882 let parallel = !matches!(num_threads, Some(n) if n <= 1);
1883 if parallel {
1884 use rayon::prelude::*;
1885 let mut run = || match values
1890 .par_chunks_mut(stride)
1891 .enumerate()
1892 .find_map_first(|(i, dest)| write_one(i, dest).err())
1893 {
1894 Some(err) => Err(err),
1895 None => Ok(()),
1896 };
1897 return match num_threads {
1898 Some(n) if n >= 2 => {
1899 let pool = rayon::ThreadPoolBuilder::new()
1900 .num_threads(n)
1901 .build()
1902 .map_err(|err| {
1903 error::LocatedParseError::new(
1904 error::ParseError::ValidationError(err.to_string()),
1905 0,
1906 0,
1907 )
1908 })?;
1909 pool.install(run)
1910 }
1911 _ => run(),
1912 };
1913 }
1914 }
1915 #[cfg(not(feature = "parallel"))]
1916 {
1917 let _ = num_threads;
1918 }
1919 for (i, dest) in values.chunks_mut(stride).enumerate() {
1920 write_one(i, dest)?;
1921 }
1922 Ok(())
1923}
1924
1925pub fn frames_from_bytes(
1927 bytes: &[u8],
1928 num_threads: Option<usize>,
1929) -> Result<Vec<types::ConFrame>, error::ParseError> {
1930 #[cfg(feature = "parallel")]
1931 {
1932 if let Some(n) = num_threads.filter(|n| *n >= 2) {
1933 let parts = parse_frames_parallel_bytes(bytes, Some(n));
1934 let mut frames = Vec::with_capacity(parts.len());
1935 for r in parts {
1936 frames.push(r?);
1937 }
1938 return Ok(frames);
1939 }
1940 }
1941 #[cfg(not(feature = "parallel"))]
1942 {
1943 let _ = num_threads;
1944 }
1945 let mut frames = Vec::new();
1946 for r in ConFrameIterator::from_bytes(bytes) {
1947 frames.push(r?);
1948 }
1949 Ok(frames)
1950}
1951
1952fn frames_from_bytes_located(
1954 bytes: &[u8],
1955 num_threads: Option<usize>,
1956) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
1957 #[cfg(feature = "parallel")]
1958 {
1959 if let Some(n) = num_threads.filter(|n| *n >= 2) {
1960 let timed = parse_bytes_timed(bytes, Some(n));
1961 return collect_located_slots(timed.slots, &timed.fail_lines);
1962 }
1963 }
1964 #[cfg(not(feature = "parallel"))]
1965 {
1966 let _ = num_threads;
1967 }
1968 collect_bytes_located(bytes)
1969}
1970
1971fn collect_bytes_located(bytes: &[u8]) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
1977 let mut frames = Vec::new();
1978 let mut iter = ConFrameIterator::from_bytes(bytes);
1979 while let Some(item) = iter.next() {
1980 match item {
1981 Ok(frame) => frames.push(frame),
1982 Err(error) => {
1983 return Err(error::LocatedParseError::new(
1984 error,
1985 iter.lines.line_no(),
1986 iter.frame_index(),
1987 ));
1988 }
1989 }
1990 }
1991 Ok(frames)
1992}
1993
1994#[cfg(feature = "parallel")]
1996fn collect_located_slots(
1997 slots: Vec<Result<types::ConFrame, error::ParseError>>,
1998 fail_lines: &[(usize, u32)],
1999) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
2000 let mut frames = Vec::with_capacity(slots.len());
2001 for (index, part) in slots.into_iter().enumerate() {
2002 match part {
2003 Ok(frame) => frames.push(frame),
2004 Err(error) => {
2005 let line = fail_lines
2006 .iter()
2007 .find(|(slot, _)| *slot == index)
2008 .map(|(_, line)| *line)
2009 .unwrap_or(0);
2010 return Err(error::LocatedParseError::new(error, line, index as u64));
2011 }
2012 }
2013 }
2014 Ok(frames)
2015}
2016
2017#[cfg(feature = "parallel")]
2018fn span_end(boundaries: &[usize], index: usize, len: usize) -> usize {
2019 if index + 1 < boundaries.len() {
2020 boundaries[index + 1]
2021 } else {
2022 len
2023 }
2024}
2025
2026#[cfg(feature = "parallel")]
2031#[derive(Copy, Clone)]
2032struct SlotWritePtr(*mut std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>);
2033
2034#[cfg(feature = "parallel")]
2035impl SlotWritePtr {
2036 unsafe fn write_slot(self, index: usize, value: Result<types::ConFrame, error::ParseError>) {
2039 unsafe {
2040 (*self.0.add(index)).write(value);
2041 }
2042 }
2043}
2044
2045#[cfg(feature = "parallel")]
2049unsafe impl Send for SlotWritePtr {}
2050
2051#[cfg(feature = "parallel")]
2054unsafe impl Sync for SlotWritePtr {}
2055
2056#[cfg(feature = "parallel")]
2061fn thread_minflt() -> i64 {
2062 #[cfg(all(target_os = "linux", not(miri)))]
2064 {
2065 unsafe {
2066 let mut usage = std::mem::zeroed::<libc::rusage>();
2067 libc::getrusage(libc::RUSAGE_THREAD, &mut usage);
2068 usage.ru_minflt
2069 }
2070 }
2071 #[cfg(not(all(target_os = "linux", not(miri))))]
2072 {
2073 0
2074 }
2075}
2076
2077#[cfg(feature = "parallel")]
2083fn sample_llc_misses() -> i64 {
2084 #[cfg(not(all(target_os = "linux", not(miri))))]
2087 {
2088 return -1;
2089 }
2090 #[cfg(all(target_os = "linux", not(miri)))]
2091 {
2092 #[repr(C)]
2093 struct PerfEventAttr {
2094 typ: u32,
2095 size: u32,
2096 config: u64,
2097 rest: [u64; 14],
2098 }
2099 let attr = PerfEventAttr {
2100 typ: 3,
2101 size: std::mem::size_of::<PerfEventAttr>() as u32,
2102 config: 2 | (1 << 16),
2104 rest: [0; 14],
2105 };
2106 let fd = unsafe {
2107 libc::syscall(
2108 libc::SYS_perf_event_open,
2109 &attr as *const PerfEventAttr,
2110 0i32,
2111 -1i32,
2112 -1i32,
2113 0u64,
2114 )
2115 };
2116 if fd < 0 {
2117 return -1;
2118 }
2119 let fd = fd as libc::c_int;
2120 let mut value = 0u64;
2121 let n = unsafe { libc::read(fd, &mut value as *mut u64 as *mut libc::c_void, 8) };
2122 unsafe {
2123 libc::close(fd);
2124 }
2125 if n == 8 { value as i64 } else { -1 }
2126 }
2127}
2128
2129#[cfg(feature = "parallel")]
2135fn numa_remote_pages() -> i64 {
2136 let online = std::fs::read_to_string("/sys/devices/system/node/online").unwrap_or_default();
2137 let online = online.trim();
2138 let nodes = if let Some((start, end)) = online.split_once('-') {
2139 let start: i64 = start.parse().unwrap_or(0);
2140 let end: i64 = end.parse().unwrap_or(0);
2141 end - start + 1
2142 } else {
2143 1
2144 };
2145 if nodes <= 1 {
2146 return 0;
2147 }
2148 let text = std::fs::read_to_string("/proc/self/numa_maps").unwrap_or_default();
2149 let mut remote = 0i64;
2150 for line in text.lines() {
2151 for token in line.split_whitespace() {
2152 if let Some(rest) = token.strip_prefix('N')
2153 && let Some((node, pages)) = rest.split_once('=')
2154 && node != "0"
2155 {
2156 remote += pages.parse::<i64>().unwrap_or(0);
2157 }
2158 }
2159 }
2160 remote
2161}
2162
2163#[cfg(feature = "parallel")]
2165fn equal_frame_stride(boundaries: &[usize]) -> bool {
2166 if boundaries.len() < 2 {
2167 return false;
2168 }
2169 let span = boundaries[1] - boundaries[0];
2170 if span == 0 || boundaries[0] != 0 {
2171 return false;
2172 }
2173 boundaries
2174 .iter()
2175 .enumerate()
2176 .all(|(index, start)| *start == index * span)
2177}
2178
2179#[cfg(feature = "parallel")]
2181fn one_range_per_worker(n: usize, workers: usize) -> Vec<(usize, usize)> {
2182 let workers = workers.max(1).min(n.max(1));
2183 let mut ranges = Vec::with_capacity(workers);
2184 let mut start = 0usize;
2185 for worker in 0..workers {
2186 let end = if worker + 1 == workers {
2187 n
2188 } else {
2189 ((worker as u128 + 1) * (n as u128) / workers as u128) as usize
2190 };
2191 if end > start {
2192 ranges.push((start, end));
2193 start = end;
2194 }
2195 }
2196 ranges
2197}
2198
2199#[cfg(feature = "parallel")]
2206fn byte_balanced_ranges(boundaries: &[usize], len: usize, workers: usize) -> Vec<(usize, usize)> {
2207 let n = boundaries.len();
2208 if n == 0 {
2209 return Vec::new();
2210 }
2211 let workers = workers.max(1).min(n);
2212 let chunks = workers.saturating_mul(4).clamp(workers, n);
2213 let origin = boundaries[0];
2214 let total = len.saturating_sub(origin).max(1) as u128;
2215 let mut ranges = Vec::with_capacity(chunks);
2216 let mut start = 0usize;
2217 for chunk in 0..chunks {
2218 if start >= n {
2219 break;
2220 }
2221 let end = if chunk + 1 == chunks {
2222 n
2223 } else {
2224 let target = origin + ((total * (chunk as u128 + 1)) / chunks as u128) as usize;
2225 let mut idx = if start + 1 < n {
2226 boundaries[start + 1..].partition_point(|&b| b < target) + start + 1
2227 } else {
2228 n
2229 };
2230 let min_end = start + 1;
2231 let reserved = chunks - chunk - 1;
2232 let max_end = n.saturating_sub(reserved);
2233 if max_end >= min_end {
2234 idx = idx.clamp(min_end, max_end);
2235 } else {
2236 idx = min_end.min(n);
2237 }
2238 idx
2239 };
2240 if end > start {
2241 ranges.push((start, end));
2242 start = end;
2243 }
2244 }
2245 if start < n {
2246 match ranges.last_mut() {
2247 Some(last) => last.1 = n,
2248 None => ranges.push((start, n)),
2249 }
2250 }
2251 ranges
2252}
2253
2254#[cfg(feature = "parallel")]
2261fn parse_into_slots<F>(
2262 bytes: &[u8],
2263 len: usize,
2264 boundaries: &[usize],
2265 workers: usize,
2266 parse_span: F,
2267) -> Vec<Result<types::ConFrame, error::ParseError>>
2268where
2269 F: Fn(usize, usize) -> Result<types::ConFrame, error::ParseError> + Sync,
2270{
2271 parse_into_slots_timed(bytes, len, boundaries, workers, parse_span).slots
2272}
2273
2274#[cfg(feature = "parallel")]
2276struct SlotParse {
2277 slots: Vec<Result<types::ConFrame, error::ParseError>>,
2278 alloc_s: f64,
2279 work_s: f64,
2280 sum_s: f64,
2281 max_s: f64,
2282 chunk_s: Vec<f64>,
2284 worker_minflt: Vec<i64>,
2286 share_frames: Vec<u64>,
2288 share_bytes: Vec<u64>,
2290 fail_lines: Vec<(usize, u32)>,
2293}
2294
2295#[cfg(feature = "parallel")]
2303fn parse_into_slots_timed<F>(
2304 bytes: &[u8],
2305 len: usize,
2306 boundaries: &[usize],
2307 workers: usize,
2308 parse_span: F,
2309) -> SlotParse
2310where
2311 F: Fn(usize, usize) -> Result<types::ConFrame, error::ParseError> + Sync,
2312{
2313 let n = boundaries.len();
2314 if n == 0 {
2315 return SlotParse {
2316 slots: Vec::new(),
2317 alloc_s: 0.0,
2318 work_s: 0.0,
2319 sum_s: 0.0,
2320 max_s: 0.0,
2321 chunk_s: Vec::new(),
2322 worker_minflt: Vec::new(),
2323 share_frames: Vec::new(),
2324 share_bytes: Vec::new(),
2325 fail_lines: Vec::new(),
2326 };
2327 }
2328 if workers < 2 {
2329 let t_alloc = std::time::Instant::now();
2330 let mut slots = Vec::with_capacity(n);
2331 let alloc_s = t_alloc.elapsed().as_secs_f64();
2332 let flt0 = thread_minflt();
2333 let t_work = std::time::Instant::now();
2334 for i in 0..n {
2335 slots.push(parse_span(boundaries[i], span_end(boundaries, i, len)));
2336 }
2337 let work_s = t_work.elapsed().as_secs_f64();
2338 let worker_minflt = vec![thread_minflt().saturating_sub(flt0)];
2339 return SlotParse {
2340 slots,
2341 alloc_s,
2342 work_s,
2343 sum_s: work_s,
2344 max_s: work_s,
2345 chunk_s: vec![work_s],
2346 worker_minflt,
2347 share_frames: vec![n as u64],
2348 share_bytes: vec![len as u64],
2349 fail_lines: Vec::new(),
2350 };
2351 }
2352 let t_alloc = std::time::Instant::now();
2353 let mut slots = take_slot_buf(n);
2354 let alloc_s = t_alloc.elapsed().as_secs_f64();
2355 let ranges = if equal_frame_stride(boundaries) {
2360 one_range_per_worker(n, workers)
2361 } else {
2362 byte_balanced_ranges(boundaries, len, workers)
2363 };
2364 let chunk_ns: Vec<std::sync::atomic::AtomicU64> = (0..ranges.len())
2365 .map(|_| std::sync::atomic::AtomicU64::new(0))
2366 .collect();
2367 let chunk_flt: Vec<std::sync::atomic::AtomicI64> = (0..ranges.len())
2368 .map(|_| std::sync::atomic::AtomicI64::new(0))
2369 .collect();
2370 let slots_ptr = SlotWritePtr(slots.as_mut_ptr());
2371 let fail_lines = std::sync::Mutex::new(Vec::<(usize, u32)>::new());
2372 let t_work = std::time::Instant::now();
2373 {
2374 let parse_range = |range_index: usize, start: usize, end: usize| {
2375 if start >= end {
2376 return;
2377 }
2378 let flt0 = thread_minflt();
2379 let t_chunk = std::time::Instant::now();
2380 let byte_start = boundaries[start];
2381 let byte_end = span_end(boundaries, end - 1, len);
2382 let wide = end - start == 1 && byte_end.saturating_sub(byte_start) >= (256 << 10);
2383 if wide {
2384 crate::parser::set_wide_workers(workers);
2385 }
2386 let mut iter = ConFrameIterator::from_bytes(&bytes[byte_start..byte_end]);
2387 OWNED_RANGES.with(|ranges| {
2388 let generation = PARSE_GEN.load(std::sync::atomic::Ordering::Relaxed);
2389 let mut owned = ranges.borrow_mut();
2390 if owned.first().map(|(seen, _, _)| *seen) != Some(generation) {
2391 owned.clear();
2392 }
2393 owned.push((generation, start, end));
2394 });
2395 let mut known_atoms = 0usize;
2396 for i in start..end {
2397 let frame = match iter.next() {
2398 Some(Ok(frame)) => {
2399 if known_atoms == 0 {
2400 known_atoms = frame.atom_data.len();
2401 let left = end - i - 1;
2402 if known_atoms > 0 && left > 0 {
2403 crate::parser::prestash_coord_shells(left, known_atoms);
2404 }
2405 }
2406 Ok(frame)
2407 }
2408 Some(Err(error)) => {
2409 let line = share_cursor(iter.lines.line_no(), bytes, byte_start);
2410 fail_lines.lock().expect("fail lines").push((i, line));
2411 Err(error)
2412 }
2413 None => {
2414 let line = share_cursor(iter.lines.line_no(), bytes, byte_start);
2415 fail_lines.lock().expect("fail lines").push((i, line));
2416 Err(error::ParseError::IncompleteFrame)
2417 }
2418 };
2419 unsafe {
2422 slots_ptr.write_slot(i, frame);
2423 }
2424 }
2425 crate::parser::clear_coord_shells();
2426 if wide {
2427 crate::parser::set_wide_workers(1);
2428 }
2429 chunk_ns[range_index].store(
2430 t_chunk.elapsed().as_nanos() as u64,
2431 std::sync::atomic::Ordering::Relaxed,
2432 );
2433 chunk_flt[range_index].store(
2434 thread_minflt().saturating_sub(flt0),
2435 std::sync::atomic::Ordering::Relaxed,
2436 );
2437 };
2438 let run_ranges = || {
2439 use rayon::prelude::*;
2440 ranges
2441 .par_iter()
2442 .enumerate()
2443 .for_each(|(range_index, &(start, end))| {
2444 parse_range(range_index, start, end);
2445 });
2446 };
2447 if rayon::current_thread_index().is_some() {
2448 run_ranges();
2449 } else {
2450 match rayon::ThreadPoolBuilder::new().num_threads(workers).build() {
2451 Ok(pool) => pool.install(run_ranges),
2452 Err(_) => {
2453 for (range_index, &(start, end)) in ranges.iter().enumerate() {
2454 parse_range(range_index, start, end);
2455 }
2456 }
2457 }
2458 }
2459 }
2460 let work_s = t_work.elapsed().as_secs_f64();
2461 let mut worker_sum_s = 0.0_f64;
2462 let mut worker_max_s = 0.0_f64;
2463 let mut chunk_s = Vec::new();
2464 let mut worker_minflt = Vec::new();
2465 let mut share_frames = Vec::new();
2466 let mut share_bytes = Vec::new();
2467 for (range_index, &(start, end)) in ranges.iter().enumerate() {
2468 if start >= end {
2469 continue;
2470 }
2471 let seconds =
2472 chunk_ns[range_index].load(std::sync::atomic::Ordering::Relaxed) as f64 / 1.0e9;
2473 chunk_s.push(seconds);
2474 worker_sum_s += seconds;
2475 worker_max_s = worker_max_s.max(seconds);
2476 worker_minflt.push(chunk_flt[range_index].load(std::sync::atomic::Ordering::Relaxed));
2477 share_frames.push((end - start) as u64);
2478 let nbytes = span_end(boundaries, end - 1, len).saturating_sub(boundaries[start]);
2479 share_bytes.push(nbytes as u64);
2480 }
2481 let slots = unsafe {
2484 let mut raw = std::mem::ManuallyDrop::new(slots);
2485 Vec::from_raw_parts(
2486 raw.as_mut_ptr() as *mut Result<types::ConFrame, error::ParseError>,
2487 n,
2488 raw.capacity(),
2489 )
2490 };
2491 let fail_lines = fail_lines.into_inner().unwrap_or_default();
2492 SlotParse {
2493 slots,
2494 alloc_s,
2495 work_s,
2496 sum_s: worker_sum_s,
2497 max_s: worker_max_s,
2498 chunk_s,
2499 worker_minflt,
2500 share_frames,
2501 share_bytes,
2502 fail_lines,
2503 }
2504}
2505
2506#[cfg(feature = "parallel")]
2507fn parse_byte_span(
2508 bytes: &[u8],
2509 start: usize,
2510 end: usize,
2511) -> Result<types::ConFrame, error::ParseError> {
2512 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
2513 match iter.next() {
2514 Some(result) => result,
2515 None => Err(error::ParseError::IncompleteFrame),
2516 }
2517}
2518
2519#[cfg(feature = "parallel")]
2520fn parse_text_span(
2521 text: &str,
2522 start: usize,
2523 end: usize,
2524) -> Result<types::ConFrame, error::ParseError> {
2525 let mut iter = ConFrameIterator::new(&text[start..end]);
2526 match iter.next() {
2527 Some(result) => result,
2528 None => Err(error::ParseError::IncompleteFrame),
2529 }
2530}
2531
2532#[cfg(feature = "parallel")]
2533struct TimedParse {
2534 slots: Vec<Result<types::ConFrame, error::ParseError>>,
2535 pool: Option<std::sync::Arc<rayon::ThreadPool>>,
2538 pool_s: f64,
2539 pool_join_s: f64,
2541 scan_s: f64,
2542 scan_minflt: i64,
2543 slot_alloc_s: f64,
2544 worker_parse_s: f64,
2545 worker_sum_s: f64,
2546 worker_max_s: f64,
2547 chunk_s: Vec<f64>,
2548 worker_minflt: Vec<i64>,
2549 share_frames: Vec<u64>,
2550 share_bytes: Vec<u64>,
2551 fail_lines: Vec<(usize, u32)>,
2553 llc_misses: i64,
2554 numa_remote: i64,
2555 scan_kind: u8,
2557}
2558
2559#[cfg(feature = "parallel")]
2565fn keep_parse_pages() {
2566 #[cfg(all(target_os = "linux", not(miri)))]
2570 unsafe {
2571 libc::mallopt(-4, 0);
2572 libc::mallopt(-1, i32::MAX);
2573 }
2574}
2575
2576#[cfg(feature = "parallel")]
2582fn take_slot_buf(
2583 n: usize,
2584) -> Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>> {
2585 let mut buf = slot_cache()
2586 .lock()
2587 .map(|mut slot| std::mem::take(&mut *slot))
2588 .unwrap_or_default();
2589 if buf.capacity() < n {
2590 buf = Vec::with_capacity(n);
2591 }
2592 unsafe { buf.set_len(n) };
2593 buf
2594}
2595
2596#[cfg(feature = "parallel")]
2597fn slot_cache()
2598-> &'static std::sync::Mutex<Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>>>
2599{
2600 static CACHE: std::sync::Mutex<
2601 Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>>,
2602 > = std::sync::Mutex::new(Vec::new());
2603 &CACHE
2604}
2605
2606#[cfg(feature = "parallel")]
2607thread_local! {
2608 static OWNED_RANGES: std::cell::RefCell<Vec<(usize, usize, usize)>> =
2609 const { std::cell::RefCell::new(Vec::new()) };
2610}
2611
2612#[cfg(feature = "parallel")]
2616static PARSE_GEN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1);
2617
2618pub fn release_parsed_frames(frames: Vec<types::ConFrame>, workers: usize) {
2624 #[cfg(feature = "parallel")]
2625 {
2626 if workers >= 2 {
2627 if let Some(pool) = shared_pool(workers) {
2628 let mut slots: Vec<Option<types::ConFrame>> =
2629 frames.into_iter().map(Some).collect();
2630 let ptr = slots.as_mut_ptr() as usize;
2631 let len = slots.len();
2632 let generation = PARSE_GEN.load(std::sync::atomic::Ordering::Relaxed);
2633 pool.broadcast(|_| {
2634 let ranges =
2635 OWNED_RANGES.with(|ranges| std::mem::take(&mut *ranges.borrow_mut()));
2636 for (seen, start, end) in ranges {
2637 if seen != generation {
2638 continue;
2639 }
2640 let end = end.min(len);
2641 let start = start.min(end);
2642 for index in start..end {
2643 unsafe {
2644 drop((*((ptr as *mut Option<types::ConFrame>).add(index))).take());
2645 }
2646 }
2647 }
2648 });
2649 drop(slots);
2650 return;
2651 }
2652 }
2653 }
2654 let _ = workers;
2655 drop(frames);
2656}
2657
2658#[cfg(feature = "parallel")]
2663#[allow(clippy::type_complexity)]
2664fn shared_pool(workers: usize) -> Option<std::sync::Arc<rayon::ThreadPool>> {
2665 use std::sync::{Arc, Mutex, OnceLock};
2666 static CACHE: OnceLock<Mutex<Vec<(usize, Arc<rayon::ThreadPool>)>>> = OnceLock::new();
2667 let cache = CACHE.get_or_init(|| Mutex::new(Vec::new()));
2668 let mut guard = cache.lock().ok()?;
2669 if let Some((_, pool)) = guard.iter().find(|(count, _)| *count == workers) {
2670 return Some(Arc::clone(pool));
2671 }
2672 keep_parse_pages();
2673 let pool = Arc::new(
2674 rayon::ThreadPoolBuilder::new()
2675 .num_threads(workers)
2676 .build()
2677 .ok()?,
2678 );
2679 guard.push((workers, Arc::clone(&pool)));
2680 Some(pool)
2681}
2682
2683#[cfg(feature = "parallel")]
2687pub fn process_minflt() -> i64 {
2688 #[cfg(all(unix, not(miri)))]
2690 {
2691 unsafe {
2692 let mut usage = std::mem::zeroed::<libc::rusage>();
2693 libc::getrusage(libc::RUSAGE_SELF, &mut usage);
2694 usage.ru_minflt
2695 }
2696 }
2697 #[cfg(not(all(unix, not(miri))))]
2698 {
2699 0
2700 }
2701}
2702
2703#[cfg(feature = "parallel")]
2705fn host_page_size() -> usize {
2706 #[cfg(all(unix, not(miri)))]
2707 {
2708 let raw = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
2709 if raw > 0 { raw as usize } else { 4096 }
2710 }
2711 #[cfg(not(all(unix, not(miri))))]
2712 {
2713 4096
2714 }
2715}
2716
2717#[cfg(feature = "parallel")]
2722fn first_touch_f64(values: &mut [f64], workers: usize) {
2723 use rayon::prelude::*;
2724 if values.is_empty() || workers < 2 {
2725 return;
2726 }
2727 let page = host_page_size().max(std::mem::size_of::<f64>());
2728 let step = (page / std::mem::size_of::<f64>()).max(1);
2729 let pages = values.len().div_ceil(step);
2730 let share = pages.div_ceil(workers);
2731 if rayon::current_thread_index().is_some() {
2732 values.par_chunks_mut(step).for_each(|chunk| {
2735 if let Some(slot) = chunk.first_mut() {
2736 *slot = 0.0;
2737 }
2738 });
2739 return;
2740 }
2741 std::thread::scope(|scope| {
2742 let mut rest = values;
2743 for worker in 0..workers {
2744 if rest.is_empty() {
2745 break;
2746 }
2747 let pages_here = share.min(rest.len().div_ceil(step));
2748 let take = (pages_here * step).min(rest.len());
2749 let (head, tail) = rest.split_at_mut(take);
2750 rest = tail;
2751 let _ = worker;
2752 scope.spawn(move || {
2753 let mut at = 0;
2754 while at < head.len() {
2755 head[at] = 0.0;
2756 at += step;
2757 }
2758 });
2759 }
2760 });
2761}
2762
2763#[cfg(feature = "parallel")]
2769fn finish_results(
2770 pool: Option<&rayon::ThreadPool>,
2771 slots: Vec<Result<types::ConFrame, error::ParseError>>,
2772) -> Result<Vec<types::ConFrame>, error::ParseError> {
2773 if slots.len() < 2 || pool.is_none() {
2774 let mut frames = Vec::with_capacity(slots.len());
2775 for part in slots {
2776 frames.push(part?);
2777 }
2778 return Ok(frames);
2779 }
2780 let pool = pool.expect("pool");
2781 pool.install(|| {
2782 use rayon::prelude::*;
2783 if let Some(index) = slots.par_iter().position_first(|item| item.is_err()) {
2784 return Err(match slots.into_iter().nth(index) {
2785 Some(Err(err)) => err,
2786 _ => error::ParseError::IncompleteFrame,
2787 });
2788 }
2789 Ok(slots
2790 .into_par_iter()
2791 .map(|item| item.expect("ok"))
2792 .collect())
2793 })
2794}
2795
2796pub const SCAN_SERIAL: u8 = 0;
2798pub const SCAN_INDEXED: u8 = 1;
2805pub const SCAN_EQUAL_SPAN: u8 = 2;
2807
2808#[cfg(feature = "parallel")]
2816#[cfg_attr(not(test), allow(dead_code))]
2817fn equal_span_boundaries(bytes: &[u8], workers: usize) -> Option<Vec<usize>> {
2818 if workers < 2 || bytes.len() < 2 {
2819 return None;
2820 }
2821 let mut first = ConFrameIterator::from_bytes(bytes);
2822 match first.forward_fast() {
2823 Some(Ok(())) => {}
2824 _ => return None,
2825 }
2826 let span = first.lines.pos;
2827 if span < 2 {
2828 return None;
2829 }
2830 let n = bytes.len() / span;
2831 if n < 2 {
2832 return None;
2833 }
2834 let tiled = n * span;
2837 if bytes[tiled..].iter().any(|b| !b.is_ascii_whitespace()) {
2838 return None;
2839 }
2840 let byte_share = tiled.div_ceil(workers);
2844 for worker in 0..workers {
2845 let byte_cut = worker.saturating_mul(byte_share).min(tiled);
2846 if byte_cut >= tiled {
2847 continue;
2848 }
2849 let index = if worker == 0 {
2850 0
2851 } else {
2852 byte_cut.div_ceil(span).min(n - 1)
2853 };
2854 let start = index.saturating_mul(span);
2855 if start >= tiled {
2856 continue;
2857 }
2858 let end = (start + span).min(tiled);
2859 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
2860 match iter.forward_fast() {
2861 Some(Ok(())) if iter.lines.pos == end - start => {}
2862 _ => return None,
2863 }
2864 }
2865 Some(arithmetic_starts(span, n))
2866}
2867
2868#[cfg(feature = "parallel")]
2873static START_CACHE: std::sync::Mutex<Vec<usize>> = std::sync::Mutex::new(Vec::new());
2874
2875#[cfg(feature = "parallel")]
2876#[cfg_attr(not(test), allow(dead_code))]
2877fn arithmetic_starts(span: usize, n: usize) -> Vec<usize> {
2878 let mut starts = START_CACHE
2879 .lock()
2880 .map(|mut slot| std::mem::take(&mut *slot))
2881 .unwrap_or_default();
2882 if starts.len() != n {
2883 starts.resize(n, 0);
2884 }
2885 for (index, slot) in starts.iter_mut().enumerate() {
2886 *slot = index * span;
2887 }
2888 starts
2889}
2890
2891#[cfg(feature = "parallel")]
2892fn recycle_starts(starts: Vec<usize>) {
2893 if let Ok(mut slot) = START_CACHE.lock()
2894 && starts.capacity() >= slot.capacity()
2895 {
2896 *slot = starts;
2897 }
2898}
2899
2900#[cfg(feature = "parallel")]
2901fn parse_frames_parallel_bytes(
2902 bytes: &[u8],
2903 num_threads: Option<usize>,
2904) -> Vec<Result<types::ConFrame, error::ParseError>> {
2905 parse_bytes_timed(bytes, num_threads).slots
2906}
2907
2908#[cfg(feature = "parallel")]
2914fn parallel_boundaries(bytes: &[u8], workers: usize) -> (Vec<usize>, u8) {
2915 if workers >= 2 {
2916 if let Some(bounds) = equal_span_boundaries(bytes, workers) {
2917 return (bounds, SCAN_EQUAL_SPAN);
2918 }
2919 let newlines = newline_positions(bytes, workers);
2920 let mut scanner = ConFrameIterator::from_bytes(bytes);
2921 scanner.lines.index_line = 0;
2922 scanner.lines.newlines = Some(newlines);
2923 return (collect_frame_starts(scanner, true), SCAN_INDEXED);
2924 }
2925 (
2926 collect_frame_starts(ConFrameIterator::from_bytes(bytes), true),
2927 SCAN_SERIAL,
2928 )
2929}
2930
2931#[cfg(feature = "parallel")]
2938fn parse_bytes_timed(bytes: &[u8], num_threads: Option<usize>) -> TimedParse {
2939 let parse_workers = match num_threads {
2940 Some(n) if n >= 2 => n,
2941 _ => 1,
2942 };
2943 let run = || {
2944 let fault0 = process_minflt();
2945 let t_scan = std::time::Instant::now();
2946 let (boundaries, scan_kind) = parallel_boundaries(bytes, parse_workers);
2950 let scan_s = t_scan.elapsed().as_secs_f64();
2951 let scan_minflt = process_minflt().saturating_sub(fault0);
2952 let parsed = parse_into_slots_timed(
2953 bytes,
2954 bytes.len(),
2955 &boundaries,
2956 parse_workers,
2957 |start, end| parse_byte_span(bytes, start, end),
2958 );
2959 recycle_starts(boundaries);
2960 (parsed, scan_s, scan_kind, scan_minflt)
2961 };
2962 let (pool_s, pool, packed) = match num_threads {
2963 None => (0.0, None, run()),
2964 Some(n) if n <= 1 => {
2965 let _ = n;
2966 keep_parse_pages();
2967 let fault0 = process_minflt();
2968 let t_scan = std::time::Instant::now();
2969 let boundaries = collect_frame_starts(ConFrameIterator::from_bytes(bytes), true);
2970 let scan_s = t_scan.elapsed().as_secs_f64();
2971 let scan_minflt = process_minflt().saturating_sub(fault0);
2972 let parsed =
2973 parse_into_slots_timed(bytes, bytes.len(), &boundaries, 1, |start, end| {
2974 parse_byte_span(bytes, start, end)
2975 });
2976 (0.0, None, (parsed, scan_s, SCAN_SERIAL, scan_minflt))
2977 }
2978 Some(n) => {
2979 let t_pool = std::time::Instant::now();
2980 match shared_pool(n) {
2981 Some(pool) => {
2982 let built = t_pool.elapsed().as_secs_f64();
2983 PARSE_GEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2984 let t_wake = std::time::Instant::now();
2985 let (wake, body) = pool.install(|| {
2986 let wake = t_wake.elapsed().as_secs_f64();
2987 (wake, run())
2988 });
2989 (built + wake, Some(pool), body)
2992 }
2993 None => (t_pool.elapsed().as_secs_f64(), None, run()),
2994 }
2995 }
2996 };
2997 let (parsed, scan_s, scan_kind, scan_minflt) = packed;
2998 TimedParse {
2999 slots: parsed.slots,
3000 pool,
3001 pool_s,
3002 pool_join_s: 0.0,
3003 scan_s,
3004 scan_minflt,
3005 slot_alloc_s: parsed.alloc_s,
3006 worker_parse_s: parsed.work_s,
3007 worker_sum_s: parsed.sum_s,
3008 worker_max_s: parsed.max_s,
3009 chunk_s: parsed.chunk_s,
3010 worker_minflt: parsed.worker_minflt,
3011 share_frames: parsed.share_frames,
3012 share_bytes: parsed.share_bytes,
3013 fail_lines: parsed.fail_lines,
3014 llc_misses: sample_llc_misses(),
3015 numa_remote: numa_remote_pages(),
3016 scan_kind,
3017 }
3018}
3019
3020pub fn count_frames(path: &Path) -> Result<usize, LoadError> {
3027 let contents = load_text(path)?;
3028 let text = utf8_text(contents.as_bytes(), true)?;
3029 let mut n = 0usize;
3030 let mut iter = ConFrameIterator::new(text);
3031 loop {
3032 match iter.forward_fast() {
3033 Some(Ok(())) => n += 1,
3034 Some(Err(e)) => {
3035 return Err(LoadError::Parse(error::LocatedParseError::new(
3036 e,
3037 iter.lines.line_no(),
3038 iter.frame_index(),
3039 )));
3040 }
3041 None => break,
3042 }
3043 }
3044 Ok(n)
3045}
3046
3047pub fn read_first_frame(path: &Path) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
3062 let contents = crate::compression::read_file_contents(path)?;
3063 let text = contents.as_str()?;
3064 let mut iter = ConFrameIterator::new(text);
3065 match iter.next() {
3066 Some(Ok(frame)) => Ok(frame),
3067 Some(Err(e)) => Err(Box::new(e)),
3068 None => Err("No frames found in file".into()),
3069 }
3070}
3071
3072pub fn read_nth_frame(
3076 path: &Path,
3077 index: usize,
3078) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
3079 let contents = crate::compression::read_file_contents(path)?;
3080 let text = contents.as_str()?;
3081 read_nth_frame_from_text(text, index)
3082}
3083
3084pub fn read_nth_frame_from_text(
3086 text: &str,
3087 index: usize,
3088) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
3089 let mut iter = ConFrameIterator::new(text);
3090 let skipped = iter.skip_frames(index)?;
3091 if skipped < index {
3092 return Err(format!("frame {index} out of range (only {skipped} frames)").into());
3093 }
3094 match iter.next() {
3095 Some(Ok(frame)) => Ok(frame),
3096 Some(Err(e)) => Err(Box::new(e)),
3097 None => Err(format!("frame {index} out of range").into()),
3098 }
3099}
3100
3101#[cfg(feature = "parallel")]
3107pub fn read_all_frames_indexed(
3108 path: &Path,
3109 num_threads: Option<usize>,
3110) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
3111 let contents = crate::compression::read_file_contents(path)?;
3112 let text = contents.as_str_sliced()?;
3113 let idx = match contents {
3114 crate::compression::FileContents::Mapped(_) => {
3115 crate::frame_index::load_or_build(path, text, true)?
3116 }
3117 crate::compression::FileContents::Owned(_) => {
3118 let on_disk = std::fs::metadata(path)?.len();
3119 crate::frame_index::load_or_build(path, text, on_disk == text.len() as u64)?
3120 }
3121 };
3122 let offs: Vec<usize> = idx.offsets.iter().map(|&o| o as usize).collect();
3123 parse_frames_at_offsets(text, &offs, num_threads)
3124 .into_iter()
3125 .map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error>))
3126 .collect()
3127}
3128
3129enum IndexStep {
3130 Stay(usize),
3131 Missing(usize),
3132 Landed(usize, usize),
3133 Short(usize, usize),
3134}
3135
3136struct IndexedSpan {
3137 start: usize,
3138 content_end: usize,
3139 next_pos: usize,
3140}
3141
3142fn line_count(nl: &[usize], len: usize) -> usize {
3144 if len == 0 {
3145 0
3146 } else if nl.last().copied() == Some(len - 1) {
3147 nl.len()
3148 } else {
3149 nl.len() + 1
3150 }
3151}
3152
3153fn line_start_pos(nl: &[usize], len: usize, line_no: usize) -> usize {
3154 if line_no == 0 {
3155 return 0;
3156 }
3157 if line_no >= line_count(nl, len) {
3158 return len;
3159 }
3160 nl[line_no - 1] + 1
3161}
3162
3163fn line_no_for_pos(nl: &[usize], len: usize, pos: usize) -> usize {
3165 if pos >= len {
3166 return line_count(nl, len);
3167 }
3168 nl.partition_point(|&p| p < pos)
3169}
3170
3171fn indexed_span(nl: &[usize], bytes: &[u8], line_no: usize) -> Option<IndexedSpan> {
3172 let len = bytes.len();
3173 if line_no >= line_count(nl, len) {
3174 return None;
3175 }
3176 let start = if line_no == 0 { 0 } else { nl[line_no - 1] + 1 };
3177 let (raw_end, next_pos) = if line_no < nl.len() {
3178 (nl[line_no], nl[line_no] + 1)
3179 } else {
3180 (len, len)
3181 };
3182 let mut content_end = raw_end;
3183 if content_end > start && bytes[content_end - 1] == b'\r' {
3184 content_end -= 1;
3185 }
3186 Some(IndexedSpan {
3187 start,
3188 content_end,
3189 next_pos,
3190 })
3191}
3192
3193#[cfg(feature = "parallel")]
3195fn equal_cuts(len: usize, n: usize) -> Vec<usize> {
3196 let n = n.max(1);
3197 let mut cuts = Vec::with_capacity(n + 1);
3198 for i in 0..=n {
3199 let at = ((i as u128) * (len as u128) / (n as u128)) as usize;
3200 let at = at.min(len);
3201 if cuts.last().copied() != Some(at) {
3202 cuts.push(at);
3203 }
3204 }
3205 if cuts.last().copied() != Some(len) {
3206 cuts.push(len);
3207 }
3208 cuts
3209}
3210
3211#[cfg(feature = "parallel")]
3218fn newline_positions_at_cuts(bytes: &[u8], cuts: &[usize]) -> Vec<usize> {
3219 let spans: Vec<(usize, usize)> = cuts
3220 .windows(2)
3221 .map(|w| (w[0].min(bytes.len()), w[1].min(bytes.len())))
3222 .filter(|(start, end)| start < end)
3223 .collect();
3224 if spans.len() <= 1 {
3225 let (start, end) = spans.first().copied().unwrap_or((0, 0));
3226 return memchr::memchr_iter(b'\n', &bytes[start..end])
3227 .map(|p| start + p)
3228 .collect();
3229 }
3230 use rayon::prelude::*;
3234 let parts: Vec<Vec<usize>> = spans
3235 .par_iter()
3236 .map(|&(start, end)| {
3237 memchr::memchr_iter(b'\n', &bytes[start..end])
3238 .map(|p| start + p)
3239 .collect()
3240 })
3241 .collect();
3242 let mut all = Vec::with_capacity(bytes.len() / 32);
3243 for part in parts {
3244 all.extend(part);
3245 }
3246 all
3247}
3248
3249#[cfg(feature = "parallel")]
3250fn newline_positions(bytes: &[u8], threads: usize) -> Vec<usize> {
3251 newline_positions_at_cuts(bytes, &equal_cuts(bytes.len(), threads))
3252}
3253
3254#[cfg(feature = "parallel")]
3264fn scan_thread_count(num_threads: Option<usize>, len: usize) -> usize {
3265 let requested = match num_threads {
3266 Some(n) if n >= 2 => n,
3267 _ => return 1,
3268 };
3269 if len < 2 { 1 } else { requested }
3270}
3271
3272#[derive(Debug)]
3274pub enum LoadError {
3275 Io(std::io::Error),
3276 Utf8(std::str::Utf8Error),
3277 Parse(error::LocatedParseError),
3278 Empty,
3280}
3281
3282impl std::fmt::Display for LoadError {
3283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3284 match self {
3285 LoadError::Io(e) => write!(f, "{e}"),
3286 LoadError::Utf8(e) => write!(f, "{e}"),
3287 LoadError::Parse(e) => write!(f, "{e}"),
3288 LoadError::Empty => write!(f, "no frames found in file"),
3289 }
3290 }
3291}
3292
3293impl std::error::Error for LoadError {}
3294
3295fn load_error_from_box(err: Box<dyn std::error::Error>) -> LoadError {
3296 if let Some(io) = err.downcast_ref::<std::io::Error>() {
3297 return LoadError::Io(std::io::Error::new(io.kind(), io.to_string()));
3298 }
3299 LoadError::Io(std::io::Error::other(err.to_string()))
3300}
3301
3302fn load_text(path: &Path) -> Result<crate::compression::FileContents, LoadError> {
3303 crate::compression::read_file_contents(path).map_err(load_error_from_box)
3304}
3305
3306#[cfg(feature = "parallel")]
3308fn file_line_at(text: &str, byte: usize) -> u32 {
3309 let end = byte.min(text.len());
3310 1 + text.as_bytes()[..end]
3311 .iter()
3312 .filter(|&&b| b == b'\n')
3313 .count() as u32
3314}
3315
3316pub fn frames_from_text_located(
3318 text: &str,
3319 num_threads: Option<usize>,
3320) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
3321 #[cfg(feature = "parallel")]
3322 {
3323 let use_parallel = matches!(num_threads, Some(n) if n >= 2);
3326 if use_parallel {
3327 let pool = num_threads.filter(|&n| n >= 2);
3328 return collect_parallel_located(text, pool);
3329 }
3330 }
3331 #[cfg(not(feature = "parallel"))]
3332 {
3333 let _ = num_threads;
3334 }
3335 let mut frames = Vec::new();
3336 let mut iter = ConFrameIterator::new(text);
3337 let mut index = 0u64;
3338 while let Some(item) = iter.next() {
3339 match item {
3340 Ok(frame) => {
3341 frames.push(frame);
3342 index += 1;
3343 }
3344 Err(e) => {
3345 return Err(error::LocatedParseError::new(
3346 e,
3347 iter.lines.line_no(),
3348 index,
3349 ));
3350 }
3351 }
3352 }
3353 Ok(frames)
3354}
3355
3356pub fn read_all_frames_located(
3358 path: &Path,
3359 num_threads: Option<usize>,
3360) -> Result<Vec<types::ConFrame>, LoadError> {
3361 if matches!(num_threads, Some(n) if n >= 2) {
3362 let contents = load_text(path)?;
3363 let text = utf8_text(contents.as_bytes(), true)?;
3364 return frames_from_text_located(text, num_threads).map_err(LoadError::Parse);
3365 }
3366 stream_frames(path)
3367}
3368
3369pub fn read_first_frame_located(path: &Path) -> Result<types::ConFrame, LoadError> {
3371 let contents = load_text(path)?;
3372 let text = utf8_text(contents.as_bytes(), true)?;
3373 let mut iter = ConFrameIterator::new(text);
3374 match iter.next() {
3375 Some(Ok(frame)) => Ok(frame),
3376 Some(Err(e)) => Err(LoadError::Parse(error::LocatedParseError::new(
3377 e,
3378 iter.lines.line_no(),
3379 0,
3380 ))),
3381 None => Err(LoadError::Empty),
3382 }
3383}
3384
3385pub fn read_nth_frame_located(path: &Path, index: usize) -> Result<types::ConFrame, LoadError> {
3387 let contents = load_text(path)?;
3388 let text = utf8_text(contents.as_bytes(), true)?;
3389 let mut iter = ConFrameIterator::new(text);
3390 match iter.skip_frames_counted(index) {
3391 Ok(skipped) if skipped == index => {}
3392 Ok(skipped) => {
3393 return Err(LoadError::Parse(error::LocatedParseError::new(
3394 error::ParseError::IncompleteFrame,
3395 iter.lines.line_no(),
3396 skipped as u64,
3397 )));
3398 }
3399 Err((failed_at, e)) => {
3400 return Err(LoadError::Parse(error::LocatedParseError::new(
3401 e,
3402 iter.lines.line_no(),
3403 failed_at as u64,
3404 )));
3405 }
3406 }
3407 match iter.next() {
3408 Some(Ok(frame)) => Ok(frame),
3409 Some(Err(e)) => Err(LoadError::Parse(error::LocatedParseError::new(
3410 e,
3411 iter.lines.line_no(),
3412 index as u64,
3413 ))),
3414 None => Err(LoadError::Parse(error::LocatedParseError::new(
3415 error::ParseError::IncompleteFrame,
3416 iter.lines.line_no(),
3417 index as u64,
3418 ))),
3419 }
3420}
3421
3422fn collect_frame_starts(scanner: ConFrameIterator<'_>, skip_whitespace_tail: bool) -> Vec<usize> {
3423 #[cfg(target_arch = "x86_64")]
3424 {
3425 if std::is_x86_feature_detected!("avx2") {
3426 return unsafe { collect_frame_starts_avx2(scanner, skip_whitespace_tail) };
3430 }
3431 }
3432 collect_frame_starts_inner(scanner, skip_whitespace_tail)
3433}
3434
3435#[cfg(target_arch = "x86_64")]
3436#[target_feature(enable = "avx2,popcnt")]
3437unsafe fn collect_frame_starts_avx2(
3438 scanner: ConFrameIterator<'_>,
3439 skip_whitespace_tail: bool,
3440) -> Vec<usize> {
3441 collect_frame_starts_inner(scanner, skip_whitespace_tail)
3442}
3443
3444#[inline(always)]
3445fn collect_frame_starts_inner(
3446 mut scanner: ConFrameIterator<'_>,
3447 skip_whitespace_tail: bool,
3448) -> Vec<usize> {
3449 let _ = skip_whitespace_tail;
3452 let mut boundaries = Vec::new();
3453 loop {
3454 scanner.lines.clear_peek();
3455 let start = scanner.lines.pos;
3456 if no_frame_at(scanner.lines.bytes, start) {
3459 break;
3460 }
3461 boundaries.push(start);
3462 match scanner.forward_fast_to(None) {
3463 Some(Ok(())) => {}
3464 Some(Err(_)) | None => break,
3465 }
3466 }
3467 boundaries
3468}
3469
3470pub fn frame_start_offsets_serial(file_contents: &str) -> Vec<usize> {
3472 collect_frame_starts(ConFrameIterator::new(file_contents), false)
3473}
3474
3475#[cfg(feature = "parallel")]
3476fn scanner_with_newlines<'a>(text: &'a str, newlines: Vec<usize>) -> ConFrameIterator<'a> {
3477 let mut scanner = ConFrameIterator::new(text);
3478 scanner.lines.index_line = 0;
3479 scanner.lines.newlines = Some(newlines);
3480 scanner
3481}
3482
3483#[cfg(feature = "parallel")]
3484fn frame_start_offsets_indexed(text: &str, newlines: Vec<usize>) -> Vec<usize> {
3485 collect_frame_starts(scanner_with_newlines(text, newlines), false)
3486}
3487
3488#[cfg(all(feature = "parallel", test))]
3494fn frame_start_offsets_at_cuts(text: &str, cuts: &[usize]) -> Vec<usize> {
3495 let newlines = newline_positions_at_cuts(text.as_bytes(), cuts);
3496 frame_start_offsets_indexed(text, newlines)
3497}
3498
3499pub fn frame_start_offsets(file_contents: &str) -> Vec<usize> {
3511 frame_start_offsets_with_threads(file_contents, None)
3512}
3513
3514pub fn frame_start_offsets_with_threads(
3522 file_contents: &str,
3523 num_threads: Option<usize>,
3524) -> Vec<usize> {
3525 #[cfg(feature = "parallel")]
3526 {
3527 let threads = scan_thread_count(num_threads, file_contents.len());
3528 if threads >= 2 {
3529 let newlines = newline_positions(file_contents.as_bytes(), threads);
3530 return frame_start_offsets_indexed(file_contents, newlines);
3531 }
3532 }
3533 #[cfg(not(feature = "parallel"))]
3534 {
3535 let _ = num_threads;
3536 }
3537 frame_start_offsets_serial(file_contents)
3538}
3539
3540#[cfg(feature = "parallel")]
3549pub fn parse_frames_parallel(
3550 file_contents: &str,
3551) -> Vec<Result<types::ConFrame, error::ParseError>> {
3552 parse_frames_parallel_with_threads(file_contents, None)
3553}
3554
3555#[cfg(feature = "parallel")]
3565pub fn parse_frames_parallel_with_threads(
3566 file_contents: &str,
3567 num_threads: Option<usize>,
3568) -> Vec<Result<types::ConFrame, error::ParseError>> {
3569 let parse_workers = match num_threads {
3570 Some(n) if n >= 2 => n,
3571 _ => 1,
3572 };
3573 let run = || {
3574 let (boundaries, _kind) = parallel_boundaries(file_contents.as_bytes(), parse_workers);
3578 parse_into_slots(
3579 file_contents.as_bytes(),
3580 file_contents.len(),
3581 &boundaries,
3582 parse_workers,
3583 |start, end| parse_text_span(file_contents, start, end),
3584 )
3585 };
3586 match num_threads {
3587 Some(n) if n >= 2 => match shared_pool(n) {
3588 Some(pool) => pool.install(run),
3589 None => run(),
3590 },
3591 _ => run(),
3592 }
3593}
3594
3595#[cfg(feature = "parallel")]
3610pub fn parse_frames_at_offsets(
3611 file_contents: &str,
3612 boundaries: &[usize],
3613 num_threads: Option<usize>,
3614) -> Vec<Result<types::ConFrame, error::ParseError>> {
3615 let workers = match num_threads {
3616 Some(n) if n <= 1 => 1,
3617 Some(n) => n,
3618 None => rayon::current_num_threads().max(1),
3619 };
3620 let run = || {
3621 parse_into_slots(
3622 file_contents.as_bytes(),
3623 file_contents.len(),
3624 boundaries,
3625 workers,
3626 |start, end| parse_text_span(file_contents, start, end),
3627 )
3628 };
3629 match num_threads {
3630 Some(n) if n >= 2 => match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
3631 Ok(pool) => pool.install(run),
3632 Err(_) => run(),
3633 },
3634 _ => run(),
3635 }
3636}
3637
3638#[derive(Clone, Copy, Debug, Default)]
3641pub struct ReadPhaseTimes {
3642 pub open_mmap_s: f64,
3643 pub pool_s: f64,
3645 pub pool_join_s: f64,
3648 pub scan_s: f64,
3649 pub slot_alloc_s: f64,
3650 pub worker_parse_s: f64,
3651 pub finish_s: f64,
3653 pub worker_sum_s: f64,
3655 pub worker_max_s: f64,
3657 pub close_s: f64,
3659 pub schedule_s: f64,
3661 pub scan_minflt: i64,
3663 pub parse_minflt: i64,
3665 pub llc_misses: i64,
3667 pub numa_remote: i64,
3669 pub slab_minflt: i64,
3671 pub touch_minflt: i64,
3673 pub mapped: bool,
3675}
3676
3677pub struct PhasedFrames {
3679 pub frames: Vec<types::ConFrame>,
3680 pub phases: ReadPhaseTimes,
3681 pub n_bytes: usize,
3682 pub chunk_s: Vec<f64>,
3684 pub scan_kind: u8,
3687 pub worker_minflt: Vec<i64>,
3689 pub share_frames: Vec<u64>,
3691 pub share_bytes: Vec<u64>,
3693 #[cfg(feature = "parallel")]
3695 pub pool: Option<std::sync::Arc<rayon::ThreadPool>>,
3696}
3697
3698#[cfg(feature = "parallel")]
3711fn piped_stock_frames(
3712 file: &[u8],
3713 threads: usize,
3714) -> Result<Option<(Vec<types::ConFrame>, f64, f64)>, Box<dyn std::error::Error>> {
3715 use std::sync::atomic::{AtomicBool, Ordering};
3716 let Some(index) = crate::compression::indexed_members(file) else {
3717 return Ok(None);
3718 };
3719 if threads < 2 {
3720 return Ok(None);
3721 }
3722 let Some(plain_total) = crate::compression::trusted_plain_total(file.len(), &index.plain)
3725 else {
3726 return Ok(None);
3727 };
3728 let Some(n) = crate::compression::trusted_row_total(plain_total, &index.rows) else {
3729 return Ok(None);
3730 };
3731 if n == 0 {
3732 return Ok(None);
3733 }
3734 let t_alloc = std::time::Instant::now();
3735 let mut offset = Vec::with_capacity(index.plain.len());
3736 let mut cursor = 0usize;
3737 for len in &index.plain {
3738 offset.push(cursor);
3739 cursor += *len;
3740 }
3741 let mut ends = index.starts[1..].to_vec();
3742 ends.push(file.len());
3743 let header: std::sync::Mutex<Option<types::FrameHeader>> = std::sync::Mutex::new(None);
3744 let symbols: std::sync::Mutex<Vec<(std::sync::Arc<str>, usize)>> =
3745 std::sync::Mutex::new(Vec::new());
3746 let ready = AtomicBool::new(false);
3747 let failed = AtomicBool::new(false);
3748 let spec = std::sync::atomic::AtomicU32::new(0);
3749 let row_origin = std::sync::atomic::AtomicUsize::new(0);
3750 let mut xyz = Vec::<f64>::new();
3751 xyz.reserve(n.saturating_mul(3));
3752 let mut ids = Vec::<u64>::new();
3753 ids.reserve(n);
3754 let mut flags = Vec::<[bool; 3]>::new();
3755 flags.reserve(n);
3756 let mut atoms = crate::types::take_atom_spare(n);
3757 unsafe { atoms.set_len(n) };
3758 let error: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
3759 let alloc_s = t_alloc.elapsed().as_secs_f64();
3760 let nworkers = threads.min(index.plain.len()).max(1);
3761 let t_scope = std::time::Instant::now();
3762 let xyz_addr = xyz.as_mut_ptr() as usize;
3763 let ids_addr = ids.as_mut_ptr() as usize;
3764 let flags_addr = flags.as_mut_ptr() as usize;
3765 let atoms_addr = atoms.as_mut_ptr() as usize;
3766 crate::parser::on_pool(nworkers, || {
3767 rayon::scope(|scope| {
3768 for worker in 0..nworkers {
3769 let index = &index;
3770 let offset = &offset;
3771 let ends = &ends;
3772 let header = &header;
3773 let symbols = &symbols;
3774 let error = &error;
3775 let ready = &ready;
3776 let failed = &failed;
3777 let spec = &spec;
3778 let row_origin = &row_origin;
3779 scope.spawn(move |_| {
3780 let xyz_ptr = xyz_addr as *mut f64;
3781 let ids_ptr = ids_addr as *mut u64;
3782 let flags_ptr = flags_addr as *mut [bool; 3];
3783 let atoms_ptr = atoms_addr as *mut std::mem::MaybeUninit<types::AtomDatum>;
3784 thread_local! {
3786 static INFLATE_BUF: std::cell::RefCell<Vec<u8>> =
3787 const { std::cell::RefCell::new(Vec::new()) };
3788 }
3789 crate::parser::pin_worker(worker);
3790 let mut symbol: Option<std::sync::Arc<str>> = None;
3791 let mut member = worker;
3792 while member < index.plain.len() {
3793 let src = &file[index.starts[member]..ends[member]];
3794 let plain_len = index.plain[member];
3795 let parsed = INFLATE_BUF.with(|cell| {
3796 let mut buf = cell.borrow_mut();
3797 buf.clear();
3798 if buf.capacity() < plain_len {
3799 let extra = plain_len - buf.len();
3800 buf.reserve(extra);
3801 }
3802 unsafe { buf.set_len(plain_len) };
3803 if let Err(err) = crate::compression::inflate_member(
3804 index.gzip,
3805 src,
3806 &mut buf[..plain_len],
3807 ) {
3808 return Err(err.to_string());
3809 }
3810 if member == 0 {
3811 match stock_header(&buf[..plain_len]) {
3812 Some((parsed, syms, origin)) => {
3813 spec.store(parsed.spec_version, Ordering::Release);
3814 row_origin.store(origin, Ordering::Release);
3815 *symbols.lock().unwrap() = syms;
3816 *header.lock().unwrap() = Some(parsed);
3817 ready.store(true, Ordering::Release);
3818 }
3819 None => {
3820 failed.store(true, Ordering::Release);
3825 return Ok(());
3826 }
3827 }
3828 }
3829 let (base, count) = index.rows[member];
3830 if count == 0 {
3831 return Ok(());
3832 }
3833 while !ready.load(Ordering::Acquire) {
3834 if failed.load(Ordering::Acquire) {
3835 return Ok(());
3836 }
3837 std::thread::yield_now();
3838 }
3839 if symbol.is_none() {
3840 symbol =
3841 symbols.lock().unwrap().first().map(|(sym, _)| sym.clone());
3842 }
3843 let Some(symbol) = symbol.as_ref() else {
3844 return Err("indexed frame has no species".into());
3845 };
3846 let spec_v = spec.load(Ordering::Acquire);
3847 let origin = row_origin.load(Ordering::Acquire);
3848 let local = if offset[member] >= origin {
3849 0
3850 } else {
3851 origin - offset[member]
3852 };
3853 let row = base as usize;
3854 let nrows = count as usize;
3855 if row.checked_add(nrows).map(|end| end <= n) != Some(true) {
3856 return Err("indexed row is outside the frame".into());
3857 }
3858 if let Err(err) = crate::parser::parse_coord_run(
3859 &buf[..plain_len],
3860 local,
3861 nrows,
3862 spec_v,
3863 row,
3864 Some(symbol),
3865 xyz_ptr,
3866 ids_ptr,
3867 flags_ptr,
3868 atoms_ptr,
3869 ) {
3870 return Err(err.to_string());
3871 }
3872 Ok(())
3873 });
3874 if let Err(err) = parsed {
3875 *error.lock().unwrap() = Some(err);
3876 failed.store(true, Ordering::Release);
3877 return;
3878 }
3879 if failed.load(Ordering::Acquire) {
3880 return;
3881 }
3882 member += nworkers;
3883 }
3884 });
3885 }
3886 });
3887 });
3888 let scope_s = t_scope.elapsed().as_secs_f64();
3889 if let Some(err) = error.into_inner().unwrap_or(None) {
3890 std::mem::forget(atoms);
3891 return Err(err.into());
3892 }
3893 let Some(header) = header.into_inner().unwrap_or(None) else {
3894 std::mem::forget(atoms);
3895 return Ok(None);
3896 };
3897 unsafe {
3900 xyz.set_len(n.saturating_mul(3));
3901 ids.set_len(n);
3902 flags.set_len(n);
3903 }
3904 let old = xyz.as_ptr();
3905 let positions = crate::storage_dtype::FloatArray2::from_f64_row_major(n, 3, xyz);
3906 let mut atoms = owned_initialized_atoms(atoms, n);
3907 crate::types::mark_uniform_symbol_drop(&atoms);
3908 if let Some(slice) = positions.f64_slice() {
3909 let new_ptr = slice.as_ptr() as *mut f64;
3910 if new_ptr != old as *mut f64 {
3911 for (index, atom) in atoms.iter_mut().enumerate() {
3912 atom.retarget(unsafe { new_ptr.add(index * 3) });
3913 }
3914 }
3915 }
3916 let frame = crate::types::con_frame_coords_only_with_ids(header, atoms, positions, ids);
3917 Ok(Some((vec![frame], alloc_s, scope_s)))
3918}
3919
3920#[cfg(feature = "parallel")]
3925fn owned_initialized_atoms(
3926 mut raw: Vec<std::mem::MaybeUninit<types::AtomDatum>>,
3927 n: usize,
3928) -> Vec<types::AtomDatum> {
3929 let cap = raw.capacity();
3930 debug_assert!(cap >= n);
3931 let ptr = raw.as_mut_ptr() as *mut types::AtomDatum;
3932 std::mem::forget(raw);
3933 unsafe { Vec::from_raw_parts(ptr, n, cap) }
3934}
3935
3936#[cfg(all(test, feature = "parallel"))]
3937mod spare_capacity {
3938 #[test]
3939 fn a_reused_spare_keeps_its_capacity() {
3940 let mut raw = crate::types::take_atom_spare(32);
3941 let cap = raw.capacity();
3942 assert!(cap >= 32);
3943 unsafe { raw.set_len(4) };
3944 for index in 0..4 {
3945 raw[index].write(crate::types::AtomDatum::coords(
3946 "Cu",
3947 0.0,
3948 0.0,
3949 0.0,
3950 [false, false, false],
3951 index as u64,
3952 ));
3953 }
3954 let atoms = super::owned_initialized_atoms(raw, 4);
3955 assert_eq!(atoms.capacity(), cap);
3956 }
3957}
3958
3959#[cfg(feature = "parallel")]
3960fn stock_header(
3961 bytes: &[u8],
3962) -> Option<(types::FrameHeader, Vec<(std::sync::Arc<str>, usize)>, usize)> {
3963 let header_region = {
3966 let mut seen = 0usize;
3967 let mut end = bytes.len();
3968 for (index, byte) in bytes.iter().enumerate() {
3969 if *byte == b'\n' {
3970 seen += 1;
3971 if seen == 64 {
3972 end = index + 1;
3973 break;
3974 }
3975 }
3976 }
3977 &bytes[..end]
3978 };
3979 let text = std::str::from_utf8(header_region).ok()?;
3980 let mut lines = text.lines();
3981 let header = crate::parser::parse_frame_header(&mut lines).ok()?;
3982 if header.natm_types != 1 || header.natms_per_type.len() != 1 {
3983 return None;
3984 }
3985 let mut symbols = Vec::with_capacity(header.natm_types);
3986 for &count in &header.natms_per_type {
3987 let symbol = lines.next()?.trim();
3988 let _label = lines.next()?;
3989 if symbol.is_empty() || count == 0 {
3990 return None;
3991 }
3992 symbols.push((std::sync::Arc::from(symbol), count));
3993 }
3994 let header_lines = 9 + header.natm_types * 2;
3995 let mut seen = 0usize;
3996 let mut origin = 0usize;
3997 for index in memchr::memchr_iter(b'\n', bytes) {
3998 seen += 1;
3999 if seen == header_lines {
4000 origin = index + 1;
4001 break;
4002 }
4003 }
4004 if seen != header_lines {
4005 return None;
4006 }
4007 Some((header, symbols, origin))
4008}
4009
4010#[cfg(feature = "parallel")]
4011fn symbol_covering(
4012 symbols: &[(std::sync::Arc<str>, usize)],
4013 row: usize,
4014) -> (&std::sync::Arc<str>, usize) {
4015 let mut acc = 0usize;
4016 for (symbol, count) in symbols {
4017 let end = acc + *count;
4018 if row < end {
4019 return (symbol, end);
4020 }
4021 acc = end;
4022 }
4023 let (symbol, count) = symbols.last().expect("species");
4024 (symbol, acc + *count)
4025}
4026
4027fn read_compressed_phased(
4028 path: &Path,
4029 num_threads: Option<usize>,
4030) -> Result<PhasedFrames, Box<dyn std::error::Error>> {
4031 let t_open = std::time::Instant::now();
4032 let file = std::fs::read(path)?;
4033 let open_s = t_open.elapsed().as_secs_f64();
4034 let t_all = std::time::Instant::now();
4035 let piped_threads = match num_threads {
4036 Some(n) if n >= 2 => n,
4037 _ => {
4038 let budget = crate::compression::decode_thread_budget();
4039 if budget >= 2 { budget } else { 1 }
4040 }
4041 };
4042 #[cfg(feature = "parallel")]
4043 if piped_threads >= 2 {
4044 if let Some((frames, alloc_s, scope_s)) = piped_stock_frames(&file, piped_threads)? {
4045 let elapsed = t_all.elapsed().as_secs_f64();
4046 let n_frames = frames.len() as u64;
4047 return Ok(PhasedFrames {
4048 frames,
4049 n_bytes: file.len(),
4050 chunk_s: vec![scope_s],
4051 scan_kind: SCAN_SERIAL,
4052 worker_minflt: Vec::new(),
4053 share_frames: vec![n_frames],
4054 share_bytes: Vec::new(),
4055 phases: ReadPhaseTimes {
4056 open_mmap_s: open_s,
4057 slot_alloc_s: alloc_s,
4058 worker_parse_s: scope_s,
4059 worker_sum_s: scope_s,
4060 worker_max_s: scope_s,
4061 finish_s: (elapsed - alloc_s - scope_s).max(0.0),
4062 ..ReadPhaseTimes::default()
4063 },
4064 pool: None,
4065 });
4066 }
4067 }
4068 crate::compression::with_inflated_plain(&file, |plain| {
4069 let open_mmap_s = open_s + t_all.elapsed().as_secs_f64();
4070 let n_bytes = plain.len();
4071 let phased = frames_from_bytes_phased_located(plain, num_threads)
4072 .map_err(|err| Box::new(err) as Box<dyn std::error::Error>)?;
4073 let elapsed = t_all.elapsed().as_secs_f64();
4074 let parse = phased.worker_parse_s
4075 + phased.scan_s
4076 + phased.slot_alloc_s
4077 + phased.finish_s
4078 + phased.pool_s
4079 + phased.pool_join_s;
4080 let schedule_s = (elapsed - open_mmap_s - parse).max(0.0);
4081 let parse_minflt = phased.worker_minflt.iter().sum();
4082 Ok(PhasedFrames {
4083 frames: phased.frames,
4084 n_bytes,
4085 chunk_s: phased.chunk_s,
4086 scan_kind: phased.scan_kind,
4087 worker_minflt: phased.worker_minflt,
4088 share_frames: phased.share_frames,
4089 share_bytes: phased.share_bytes,
4090 phases: ReadPhaseTimes {
4091 open_mmap_s,
4092 pool_s: phased.pool_s,
4093 pool_join_s: phased.pool_join_s,
4094 scan_s: phased.scan_s,
4095 slot_alloc_s: phased.slot_alloc_s,
4096 worker_parse_s: phased.worker_parse_s,
4097 finish_s: phased.finish_s,
4098 worker_sum_s: phased.worker_sum_s,
4099 worker_max_s: phased.worker_max_s,
4100 schedule_s,
4101 scan_minflt: phased.scan_minflt,
4102 parse_minflt,
4103 llc_misses: phased.llc_misses,
4104 numa_remote: phased.numa_remote,
4105 ..ReadPhaseTimes::default()
4106 },
4107 #[cfg(feature = "parallel")]
4108 pool: phased.pool,
4109 })
4110 })
4111}
4112
4113fn path_is_compressed(path: &Path) -> bool {
4114 let mut file = match std::fs::File::open(path) {
4115 Ok(file) => file,
4116 Err(_) => return false,
4117 };
4118 let mut magic = [0u8; 4];
4119 let n = std::io::Read::read(&mut file, &mut magic).unwrap_or(0);
4120 !matches!(
4121 crate::compression::detect_compression(&magic[..n]),
4122 crate::compression::Compression::None
4123 )
4124}
4125
4126pub fn read_all_frames_phased(
4127 path: &Path,
4128 num_threads: Option<usize>,
4129) -> Result<PhasedFrames, Box<dyn std::error::Error>> {
4130 if path_is_compressed(path) {
4131 return read_compressed_phased(path, num_threads);
4132 }
4133 if !matches!(num_threads, Some(n) if n >= 2) {
4134 let t_all = std::time::Instant::now();
4135 let frames = stream_frames(path).map_err(public_load_error)?;
4136 let elapsed = t_all.elapsed().as_secs_f64();
4137 let n_frames = frames.len() as u64;
4138 return Ok(PhasedFrames {
4139 frames,
4140 n_bytes: 0,
4141 chunk_s: vec![elapsed],
4142 scan_kind: SCAN_SERIAL,
4143 worker_minflt: Vec::new(),
4144 share_frames: vec![n_frames],
4145 share_bytes: Vec::new(),
4146 phases: ReadPhaseTimes {
4147 worker_parse_s: elapsed,
4148 worker_sum_s: elapsed,
4149 worker_max_s: elapsed,
4150 ..ReadPhaseTimes::default()
4151 },
4152 #[cfg(feature = "parallel")]
4153 pool: None,
4154 });
4155 }
4156 let t_all = std::time::Instant::now();
4157 let t_open = std::time::Instant::now();
4158 let contents = crate::compression::read_file_contents(path)?;
4159 let open_mmap_s = t_open.elapsed().as_secs_f64();
4160 let mapped = matches!(contents, crate::compression::FileContents::Mapped(_));
4161 let n_bytes = match &contents {
4162 crate::compression::FileContents::Owned(text) => text.len(),
4163 crate::compression::FileContents::Mapped(map) => map.len(),
4164 };
4165 let bytes: &[u8] = match &contents {
4166 crate::compression::FileContents::Owned(text) => text.as_bytes(),
4167 crate::compression::FileContents::Mapped(map) => map,
4168 };
4169 let phased = frames_from_bytes_phased_located(bytes, num_threads)?;
4170 let workers = match num_threads {
4171 Some(n) if n >= 2 => n,
4172 _ => 1,
4173 };
4174 let t_close = std::time::Instant::now();
4175 if let crate::compression::FileContents::Mapped(map) = &contents {
4176 if workers >= 2 {
4181 #[cfg(feature = "parallel")]
4182 {
4183 if let Some(pool) = phased.pool.as_ref() {
4184 pool.install(|| {
4185 crate::compression::advise_dontneed_parallel(map, workers);
4186 });
4187 }
4188 }
4189 #[cfg(not(feature = "parallel"))]
4190 {
4191 let _ = (map, workers);
4192 }
4193 }
4194 }
4195 drop(contents);
4196 let close_s = t_close.elapsed().as_secs_f64();
4197 let elapsed = t_all.elapsed().as_secs_f64();
4198 let named = open_mmap_s
4199 + phased.pool_s
4200 + phased.pool_join_s
4201 + phased.scan_s
4202 + phased.slot_alloc_s
4203 + phased.worker_parse_s
4204 + phased.finish_s
4205 + close_s;
4206 let schedule_s = (elapsed - named).max(0.0);
4207 let parse_minflt = phased.worker_minflt.iter().sum();
4208 Ok(PhasedFrames {
4209 frames: phased.frames,
4210 n_bytes,
4211 chunk_s: phased.chunk_s,
4212 scan_kind: phased.scan_kind,
4213 worker_minflt: phased.worker_minflt,
4214 share_frames: phased.share_frames,
4215 share_bytes: phased.share_bytes,
4216 phases: ReadPhaseTimes {
4217 open_mmap_s,
4218 pool_s: phased.pool_s,
4219 pool_join_s: phased.pool_join_s,
4220 scan_s: phased.scan_s,
4221 slot_alloc_s: phased.slot_alloc_s,
4222 worker_parse_s: phased.worker_parse_s,
4223 finish_s: phased.finish_s,
4224 worker_sum_s: phased.worker_sum_s,
4225 worker_max_s: phased.worker_max_s,
4226 close_s,
4227 schedule_s,
4228 scan_minflt: phased.scan_minflt,
4229 parse_minflt,
4230 llc_misses: phased.llc_misses,
4231 numa_remote: phased.numa_remote,
4232 mapped,
4233 ..ReadPhaseTimes::default()
4234 },
4235 #[cfg(feature = "parallel")]
4236 pool: phased.pool,
4237 })
4238}
4239
4240pub struct PhasedByteTimes {
4246 pub frames: Vec<types::ConFrame>,
4247 pub pool_s: f64,
4248 pub pool_join_s: f64,
4249 pub scan_s: f64,
4250 pub slot_alloc_s: f64,
4251 pub worker_parse_s: f64,
4252 pub finish_s: f64,
4253 pub worker_sum_s: f64,
4254 pub worker_max_s: f64,
4255 pub chunk_s: Vec<f64>,
4257 pub scan_kind: u8,
4260 pub scan_minflt: i64,
4262 pub worker_minflt: Vec<i64>,
4264 pub share_frames: Vec<u64>,
4265 pub share_bytes: Vec<u64>,
4266 pub llc_misses: i64,
4268 pub numa_remote: i64,
4270 #[cfg(feature = "parallel")]
4272 pub pool: Option<std::sync::Arc<rayon::ThreadPool>>,
4273}
4274
4275#[cfg(feature = "parallel")]
4281fn declared_wide_frame(bytes: &[u8]) -> bool {
4282 let mut pos = 0usize;
4283 let mut natypes = 0usize;
4284 for line_no in 0..8 {
4285 if pos >= bytes.len() {
4286 return false;
4287 }
4288 let end = memchr::memchr(b'\n', &bytes[pos..])
4289 .map(|rel| pos + rel)
4290 .unwrap_or(bytes.len());
4291 let line = &bytes[pos..end];
4292 if line_no == 6 {
4293 natypes = match first_header_usize(line) {
4294 Some(n) if (1..=64).contains(&n) => n,
4295 _ => return false,
4296 };
4297 } else if line_no == 7 {
4298 let mut sum = 0usize;
4299 let mut seen = 0usize;
4300 for field in line
4301 .split(|byte| byte.is_ascii_whitespace())
4302 .filter(|field| !field.is_empty())
4303 {
4304 let Some(n) = first_header_usize(field) else {
4305 return false;
4306 };
4307 if n == 0 {
4308 return false;
4309 }
4310 sum = sum.saturating_add(n);
4311 seen += 1;
4312 }
4313 if seen != natypes || sum < 4096 {
4314 return false;
4315 }
4316 return bytes.len() <= sum.saturating_mul(64).saturating_add(4096);
4317 }
4318 pos = end.saturating_add(1);
4319 }
4320 false
4321}
4322
4323#[cfg(feature = "parallel")]
4324fn first_header_usize(line: &[u8]) -> Option<usize> {
4325 let field = line
4326 .split(|byte| byte.is_ascii_whitespace())
4327 .find(|field| !field.is_empty())?;
4328 std::str::from_utf8(field).ok()?.parse().ok()
4329}
4330
4331#[cfg(feature = "parallel")]
4332fn wide_frame_phased(
4333 bytes: &[u8],
4334 workers: usize,
4335) -> Result<PhasedByteTimes, error::LocatedParseError> {
4336 struct Reset;
4337 impl Drop for Reset {
4338 fn drop(&mut self) {
4339 crate::parser::set_wide_workers(1);
4340 }
4341 }
4342 crate::parser::set_wide_workers(workers);
4343 let _reset = Reset;
4344 let flt0 = thread_minflt();
4345 let t_work = std::time::Instant::now();
4346 let frames = collect_bytes_located(bytes)?;
4347 let worker_parse_s = t_work.elapsed().as_secs_f64();
4348 let worker_flt = thread_minflt().saturating_sub(flt0);
4349 let n_frames = frames.len() as u64;
4350 Ok(PhasedByteTimes {
4351 frames,
4352 pool_s: 0.0,
4353 pool_join_s: 0.0,
4354 scan_s: 0.0,
4355 slot_alloc_s: 0.0,
4356 worker_parse_s,
4357 finish_s: 0.0,
4358 worker_sum_s: worker_parse_s,
4359 worker_max_s: worker_parse_s,
4360 chunk_s: vec![worker_parse_s],
4361 scan_kind: SCAN_SERIAL,
4362 scan_minflt: 0,
4363 worker_minflt: vec![worker_flt],
4364 share_frames: vec![n_frames],
4365 share_bytes: vec![bytes.len() as u64],
4366 llc_misses: sample_llc_misses(),
4367 numa_remote: numa_remote_pages(),
4368 pool: None,
4369 })
4370}
4371
4372pub fn frames_from_bytes_phased(
4373 bytes: &[u8],
4374 num_threads: Option<usize>,
4375) -> Result<PhasedByteTimes, error::ParseError> {
4376 frames_from_bytes_phased_located(bytes, num_threads).map_err(|err| err.error)
4377}
4378
4379fn frames_from_bytes_phased_located(
4380 bytes: &[u8],
4381 num_threads: Option<usize>,
4382) -> Result<PhasedByteTimes, error::LocatedParseError> {
4383 #[cfg(feature = "parallel")]
4384 {
4385 if !matches!(num_threads, Some(n) if n >= 2) {
4388 let flt0 = thread_minflt();
4389 let t_work = std::time::Instant::now();
4390 let frames = collect_bytes_located(bytes)?;
4391 let worker_parse_s = t_work.elapsed().as_secs_f64();
4392 let worker_flt = thread_minflt().saturating_sub(flt0);
4393 let n_frames = frames.len() as u64;
4394 return Ok(PhasedByteTimes {
4395 frames,
4396 pool_s: 0.0,
4397 pool_join_s: 0.0,
4398 scan_s: 0.0,
4399 slot_alloc_s: 0.0,
4400 worker_parse_s,
4401 finish_s: 0.0,
4402 worker_sum_s: worker_parse_s,
4403 worker_max_s: worker_parse_s,
4404 chunk_s: vec![worker_parse_s],
4405 scan_kind: SCAN_SERIAL,
4406 scan_minflt: 0,
4407 worker_minflt: vec![worker_flt],
4408 share_frames: vec![n_frames],
4409 share_bytes: vec![bytes.len() as u64],
4410 llc_misses: sample_llc_misses(),
4411 numa_remote: numa_remote_pages(),
4412 pool: None,
4413 });
4414 }
4415 if let Some(workers) = num_threads.filter(|n| *n >= 2 && declared_wide_frame(bytes)) {
4420 return wide_frame_phased(bytes, workers);
4421 }
4422 let requested = Some(num_threads.expect("workers"));
4423 let timed = parse_bytes_timed(bytes, requested);
4424 let t_finish = std::time::Instant::now();
4425 let frames = if timed.slots.iter().any(|item| item.is_err()) {
4426 collect_located_slots(timed.slots, &timed.fail_lines)?
4427 } else {
4428 finish_results(timed.pool.as_deref(), timed.slots)
4429 .map_err(|error| error::LocatedParseError::new(error, 0, 0))?
4430 };
4431 let finish_s = t_finish.elapsed().as_secs_f64();
4432 Ok(PhasedByteTimes {
4433 frames,
4434 pool_s: timed.pool_s,
4435 pool_join_s: timed.pool_join_s,
4436 scan_s: timed.scan_s,
4437 slot_alloc_s: timed.slot_alloc_s,
4438 worker_parse_s: timed.worker_parse_s,
4439 finish_s,
4440 worker_sum_s: timed.worker_sum_s,
4441 worker_max_s: timed.worker_max_s,
4442 chunk_s: timed.chunk_s,
4443 scan_kind: timed.scan_kind,
4444 scan_minflt: timed.scan_minflt,
4445 worker_minflt: timed.worker_minflt,
4446 share_frames: timed.share_frames,
4447 share_bytes: timed.share_bytes,
4448 llc_misses: timed.llc_misses,
4449 numa_remote: timed.numa_remote,
4450 pool: timed.pool,
4451 })
4452 }
4453 #[cfg(not(feature = "parallel"))]
4454 {
4455 let _ = num_threads;
4456 let t_work = std::time::Instant::now();
4457 let frames = collect_bytes_located(bytes)?;
4458 let worker_parse_s = t_work.elapsed().as_secs_f64();
4459 Ok(PhasedByteTimes {
4460 frames,
4461 pool_s: 0.0,
4462 pool_join_s: 0.0,
4463 scan_s: 0.0,
4464 slot_alloc_s: 0.0,
4465 worker_parse_s,
4466 finish_s: 0.0,
4467 worker_sum_s: worker_parse_s,
4468 worker_max_s: worker_parse_s,
4469 chunk_s: vec![worker_parse_s],
4470 scan_kind: 0,
4471 scan_minflt: 0,
4472 worker_minflt: Vec::new(),
4473 share_frames: Vec::new(),
4474 share_bytes: Vec::new(),
4475 llc_misses: -1,
4476 numa_remote: 0,
4477 })
4478 }
4479}
4480
4481pub fn pack_position_slab(frames: &[types::ConFrame], workers: usize) -> Option<Vec<f64>> {
4487 if frames.is_empty() {
4488 return None;
4489 }
4490 let n_atoms = frames[0].positions.nrows();
4491 if frames
4492 .iter()
4493 .any(|frame| frame.positions.nrows() != n_atoms)
4494 {
4495 return None;
4496 }
4497 let stride = n_atoms.saturating_mul(3);
4498 let total = frames.len().saturating_mul(stride);
4499 let mut values = Vec::<f64>::with_capacity(total);
4500 if total > 0 {
4501 unsafe {
4505 values.set_len(total);
4506 }
4507 }
4508 if stride == 0 {
4509 return Some(values);
4510 }
4511 let write = |frame: &types::ConFrame, dest: &mut [f64]| {
4512 if let Some(src) = frame.positions.f64_slice() {
4513 dest.copy_from_slice(src);
4514 } else {
4515 for (row, slot) in dest.as_chunks_mut::<3>().0.iter_mut().enumerate() {
4516 slot.copy_from_slice(&frame.positions.as_f64_row(row));
4517 }
4518 }
4519 };
4520 #[cfg(feature = "parallel")]
4521 {
4522 if workers >= 2 {
4523 let chunk_frames = frames.len().div_ceil(workers).max(1);
4524 let chunk_vals = chunk_frames.saturating_mul(stride);
4525 let copy_chunks = |dest_all: &mut [f64]| {
4526 use rayon::prelude::*;
4527 dest_all
4528 .par_chunks_mut(chunk_vals)
4529 .enumerate()
4530 .for_each(|(chunk_index, dest)| {
4531 let base = chunk_index * chunk_frames;
4532 for (offset, slot) in dest.chunks_exact_mut(stride).enumerate() {
4533 let index = base + offset;
4534 if index < frames.len() {
4535 write(&frames[index], slot);
4536 }
4537 }
4538 });
4539 };
4540 if rayon::current_thread_index().is_some() {
4544 first_touch_f64(&mut values, workers);
4545 copy_chunks(&mut values);
4546 } else {
4547 first_touch_f64(&mut values, workers);
4548 std::thread::scope(|scope| {
4549 let mut rest = &mut values[..];
4550 let mut base = 0usize;
4551 let mut handles = Vec::new();
4552 while base < frames.len() {
4553 let take_frames = chunk_frames.min(frames.len() - base);
4554 let take_vals = take_frames * stride;
4555 let (dest, tail) = rest.split_at_mut(take_vals);
4556 rest = tail;
4557 let frame_slice = &frames[base..base + take_frames];
4558 handles.push(scope.spawn(move || {
4559 for (frame, slot) in
4560 frame_slice.iter().zip(dest.chunks_exact_mut(stride))
4561 {
4562 write(frame, slot);
4563 }
4564 }));
4565 base += take_frames;
4566 }
4567 for handle in handles {
4568 handle.join().expect("slab worker");
4569 }
4570 });
4571 }
4572 return Some(values);
4573 }
4574 }
4575 #[cfg(not(feature = "parallel"))]
4576 {
4577 let _ = workers;
4578 }
4579 for (frame, dest) in frames.iter().zip(values.chunks_exact_mut(stride)) {
4580 write(frame, dest);
4581 }
4582 Some(values)
4583}
4584
4585fn mix_touch(hash: u64, value: u64) -> u64 {
4586 hash.wrapping_mul(0x9E3779B185EBCA87) ^ value
4587}
4588
4589fn one_frame_touch(frame: &types::ConFrame, coords: &[f64], index: u64) -> u64 {
4590 let mut hash = index;
4591 for value in frame.header.boxl {
4592 hash = mix_touch(hash, value.to_bits());
4593 }
4594 for atom in &frame.atom_data {
4595 hash = mix_touch(hash, atom.symbol.len() as u64);
4596 for byte in atom.symbol.as_bytes() {
4597 hash = mix_touch(hash, u64::from(*byte));
4598 }
4599 }
4600 for value in coords {
4601 hash = mix_touch(hash, value.to_bits());
4602 }
4603 hash
4604}
4605
4606pub fn touch_frame_checksum(
4613 frames: &[types::ConFrame],
4614 positions: &[f64],
4615 workers: usize,
4616) -> Option<u64> {
4617 if frames.is_empty() {
4618 return Some(0);
4619 }
4620 let n_atoms = frames[0].atom_data.len();
4621 let stride = n_atoms.saturating_mul(3);
4622 if stride == 0 || positions.len() != frames.len().saturating_mul(stride) {
4623 return None;
4624 }
4625 if frames.iter().any(|frame| frame.atom_data.len() != n_atoms) {
4626 return None;
4627 }
4628 let one = |index: usize| {
4629 let start = index * stride;
4630 one_frame_touch(
4631 &frames[index],
4632 &positions[start..start + stride],
4633 index as u64,
4634 )
4635 };
4636 Some(reduce_touch(frames.len(), workers, one))
4637}
4638
4639pub fn touch_option_checksum(
4643 frames: &[Option<types::ConFrame>],
4644 positions: &[f64],
4645 workers: usize,
4646) -> Option<u64> {
4647 if frames.is_empty() {
4648 return Some(0);
4649 }
4650 if frames.iter().any(Option::is_none) {
4651 return None;
4652 }
4653 let n_atoms = frames[0].as_ref()?.atom_data.len();
4654 let stride = n_atoms.saturating_mul(3);
4655 if stride == 0 || positions.len() != frames.len().saturating_mul(stride) {
4656 return None;
4657 }
4658 if frames
4659 .iter()
4660 .any(|frame| frame.as_ref().map(|item| item.atom_data.len()) != Some(n_atoms))
4661 {
4662 return None;
4663 }
4664 let one = |index: usize| {
4665 let start = index * stride;
4666 one_frame_touch(
4667 frames[index].as_ref().expect("slot"),
4668 &positions[start..start + stride],
4669 index as u64,
4670 )
4671 };
4672 Some(reduce_touch(frames.len(), workers, one))
4673}
4674
4675fn reduce_touch(n: usize, workers: usize, one: impl Fn(usize) -> u64 + Sync) -> u64 {
4676 #[cfg(feature = "parallel")]
4677 if workers >= 2 {
4678 let fold_range = |first: usize, last: usize| {
4679 let mut acc = 0u64;
4680 for index in first..last {
4681 acc ^= one(index);
4682 }
4683 acc
4684 };
4685 let chunk = n.div_ceil(workers).max(1);
4686 if rayon::current_thread_index().is_some() {
4689 use rayon::prelude::*;
4690 return (0..workers)
4691 .into_par_iter()
4692 .map(|worker| {
4693 let first = worker * chunk;
4694 let last = (first + chunk).min(n);
4695 fold_range(first, last)
4696 })
4697 .reduce(|| 0u64, |left, right| left ^ right);
4698 }
4699 let mut acc = 0u64;
4700 std::thread::scope(|scope| {
4701 let mut handles = Vec::with_capacity(workers);
4702 for worker in 0..workers {
4703 handles.push(scope.spawn(move || {
4704 let first = worker * chunk;
4705 let last = (first + chunk).min(n);
4706 fold_range(first, last)
4707 }));
4708 }
4709 for handle in handles {
4710 acc ^= handle.join().unwrap_or(0);
4711 }
4712 });
4713 return acc;
4714 }
4715 #[cfg(not(feature = "parallel"))]
4716 {
4717 let _ = workers;
4718 }
4719 (0..n).map(one).fold(0, |acc, item| acc ^ item)
4720}
4721
4722pub fn measure_newline_scan(text: &str, threads: usize, chunked: bool) -> (Vec<usize>, f64) {
4730 #[cfg(feature = "parallel")]
4731 {
4732 if chunked && threads >= 2 {
4733 let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build();
4734 let t_scan = std::time::Instant::now();
4735 let offsets = match pool {
4736 Ok(pool) => pool.install(|| {
4737 let newlines = newline_positions(text.as_bytes(), threads);
4738 frame_start_offsets_indexed(text, newlines)
4739 }),
4740 Err(_) => {
4741 let newlines = newline_positions(text.as_bytes(), threads);
4742 frame_start_offsets_indexed(text, newlines)
4743 }
4744 };
4745 return (offsets, t_scan.elapsed().as_secs_f64());
4746 }
4747 }
4748 #[cfg(not(feature = "parallel"))]
4749 {
4750 let _ = (threads, chunked);
4751 }
4752 let t_scan = std::time::Instant::now();
4753 let offsets = frame_start_offsets_serial(text);
4754 (offsets, t_scan.elapsed().as_secs_f64())
4755}
4756
4757#[cfg(feature = "parallel")]
4760fn collect_parallel_located(
4761 file_contents: &str,
4762 num_threads: Option<usize>,
4763) -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
4764 use rayon::prelude::*;
4765
4766 let boundaries = frame_start_offsets(file_contents);
4767 let parse_chunks = || -> Result<Vec<types::ConFrame>, error::LocatedParseError> {
4768 let num_frames = boundaries.len();
4769 let parts: Vec<Result<types::ConFrame, error::LocatedParseError>> = (0..num_frames)
4770 .into_par_iter()
4771 .map(|i| {
4772 let start = boundaries[i];
4773 let end = if i + 1 < num_frames {
4774 boundaries[i + 1]
4775 } else {
4776 file_contents.len()
4777 };
4778 let chunk = &file_contents[start..end];
4779 let mut iter = ConFrameIterator::new(chunk);
4780 match iter.next() {
4781 Some(Ok(frame)) => Ok(frame),
4782 Some(Err(e)) => {
4783 let local = iter.lines.line_no();
4784 let base = file_line_at(file_contents, start);
4785 let cursor = if local == 0 { base } else { base + local - 1 };
4786 Err(error::LocatedParseError::new(e, cursor, i as u64))
4787 }
4788 None => Err(error::LocatedParseError::new(
4789 error::ParseError::IncompleteFrame,
4790 file_line_at(file_contents, start),
4791 i as u64,
4792 )),
4793 }
4794 })
4795 .collect();
4796 let mut frames = Vec::with_capacity(parts.len());
4797 for part in parts {
4798 frames.push(part?);
4799 }
4800 Ok(frames)
4801 };
4802 match num_threads {
4803 None => parse_chunks(),
4804 Some(n) => {
4805 let n = n.max(1);
4806 match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
4807 Ok(pool) => pool.install(parse_chunks),
4808 Err(_) => parse_chunks(),
4809 }
4810 }
4811 }
4812}
4813
4814#[cfg(all(test, feature = "parallel"))]
4815mod parallel_strong_scale_tests {
4816 use super::*;
4817 use std::path::PathBuf;
4818
4819 fn multi_frame_fixture() -> String {
4820 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con");
4821 let one = std::fs::read_to_string(p).expect("fixture");
4822 one.repeat(8)
4824 }
4825
4826 fn sequential_frames(text: &str) -> Vec<types::ConFrame> {
4827 ConFrameIterator::new(text)
4828 .map(|r| r.expect("seq frame"))
4829 .collect()
4830 }
4831
4832 fn frames_payload_key(f: &types::ConFrame) -> (usize, Vec<(String, f64, f64, f64)>) {
4833 let atoms: Vec<_> = f
4834 .atom_data
4835 .iter()
4836 .map(|a| (a.symbol.to_string(), a.x(), a.y(), a.z()))
4837 .collect();
4838 (f.atom_data.len(), atoms)
4839 }
4840
4841 #[test]
4842 fn parallel_workers_match_sequential_payloads() {
4843 let text = multi_frame_fixture();
4844 let seq = sequential_frames(&text);
4845 assert!(seq.len() >= 8);
4846 let seq_keys: Vec<_> = seq.iter().map(frames_payload_key).collect();
4847
4848 for workers in [1usize, 2, 4] {
4849 let par = parse_frames_parallel_with_threads(&text, Some(workers));
4850 assert_eq!(par.len(), seq.len(), "workers={workers}");
4851 let par_keys: Vec<_> = par
4852 .into_iter()
4853 .map(|r| frames_payload_key(&r.expect("par frame")))
4854 .collect();
4855 assert_eq!(par_keys, seq_keys, "workers={workers} frame payloads");
4856 }
4857
4858 let par_default = parse_frames_parallel(&text);
4860 let def_keys: Vec<_> = par_default
4861 .into_iter()
4862 .map(|r| frames_payload_key(&r.expect("par")))
4863 .collect();
4864 assert_eq!(def_keys, seq_keys);
4865 }
4866
4867 fn live_threads() -> usize {
4868 std::fs::read_to_string("/proc/self/status")
4869 .unwrap()
4870 .lines()
4871 .find(|line| line.starts_with("Threads:"))
4872 .unwrap()
4873 .split_whitespace()
4874 .nth(1)
4875 .unwrap()
4876 .parse()
4877 .unwrap()
4878 }
4879
4880 #[test]
4882 fn default_read_of_a_wide_file_stays_on_the_calling_thread() {
4883 const CHILD: &str = "READCON_DEFAULT_THREAD_CHILD";
4884 const NAME: &str = "iterators::parallel_strong_scale_tests::default_read_of_a_wide_file_stays_on_the_calling_thread";
4885 if std::env::var_os(CHILD).is_none() {
4886 let out = std::process::Command::new(std::env::current_exe().unwrap())
4887 .args([NAME, "--exact", "--test-threads=1"])
4888 .env(CHILD, "1")
4889 .output()
4890 .unwrap();
4891 let stdout = String::from_utf8_lossy(&out.stdout);
4892 assert!(
4893 out.status.success() && stdout.contains("test result: ok. 1 passed;"),
4894 "{stdout}{}",
4895 String::from_utf8_lossy(&out.stderr)
4896 );
4897 return;
4898 }
4899 let dir = tempfile::tempdir().unwrap();
4900 let path = dir.path().join("wide.con");
4901 let one = std::fs::read_to_string(
4902 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con"),
4903 )
4904 .unwrap();
4905 std::fs::write(&path, one.repeat(200)).unwrap();
4906 assert!(std::fs::metadata(&path).unwrap().len() > 64 * 1024);
4907 let before = live_threads();
4908 let frames = super::read_all_frames(&path).unwrap();
4909 let after = live_threads();
4910 assert_eq!(before, after, "default read started worker threads");
4911 assert!(frames.len() > 1);
4912 let text = std::fs::read_to_string(&path).unwrap();
4913 let sequential = super::frames_from_text(&text, None).unwrap();
4914 assert_eq!(sequential.len(), frames.len());
4915 assert_eq!(
4916 sequential[0].atom_data[0].x(),
4917 frames[0].atom_data[0].x(),
4918 "default read changed a coordinate"
4919 );
4920 }
4921}
4922
4923#[cfg(all(test, feature = "parallel"))]
4924mod parallel_header_scan_tests {
4925 use super::*;
4926 use std::path::PathBuf;
4927
4928 fn fixture(name: &str) -> String {
4929 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
4930 .join("resources/test")
4931 .join(name);
4932 std::fs::read_to_string(path).expect(name)
4933 }
4934
4935 fn assert_crlf_gap_cut(text: &str) {
4937 let bytes = text.as_bytes();
4938 let mut found = 0usize;
4939 let mut i = 0usize;
4940 while i + 1 < bytes.len() && found < 6 {
4941 if bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
4942 let cut = i + 1;
4943 assert_eq!(bytes[cut - 1], b'\r');
4944 assert_eq!(bytes[cut], b'\n');
4945 assert_cuts_match(text, &[0, cut, bytes.len()]);
4946 found += 1;
4947 i += 2;
4948 continue;
4949 }
4950 i += 1;
4951 }
4952 assert!(found > 0, "fixture has no CRLF pair");
4953 }
4954
4955 fn assert_cuts_match(text: &str, cuts: &[usize]) {
4956 let serial_nl: Vec<usize> = memchr::memchr_iter(b'\n', text.as_bytes()).collect();
4957 let stitched = newline_positions_at_cuts(text.as_bytes(), cuts);
4958 assert_eq!(stitched, serial_nl, "newline stitch at {cuts:?}");
4959 let serial = frame_start_offsets_serial(text);
4960 let parallel = frame_start_offsets_at_cuts(text, cuts);
4961 assert_eq!(parallel, serial, "frame offsets at {cuts:?}");
4962 }
4963
4964 fn header_straddle_cuts(text: &str) -> Vec<Vec<usize>> {
4965 let serial = frame_start_offsets_serial(text);
4966 let bytes = text.as_bytes();
4967 let mut out = Vec::new();
4968 for n in [2usize, 3, 4, 8] {
4970 out.push(equal_cuts(bytes.len(), n));
4971 }
4972 for (index, &start) in serial.iter().enumerate() {
4973 let next = serial.get(index + 1).copied().unwrap_or(bytes.len());
4974 let window = super::header_window_end(bytes, start, next);
4977 let mut line = start;
4978 while line < window {
4979 let Some(rel) = memchr::memchr(b'\n', &bytes[line..window]) else {
4980 break;
4981 };
4982 let end = line + rel;
4983 if end - line > 1 {
4984 let mid = line + (end - line) / 2;
4985 assert!(mid > line && mid < end);
4987 out.push(vec![0, mid, bytes.len()]);
4988 }
4989 line = end + 1;
4990 }
4991 }
4992 out
4993 }
4994
4995 fn assert_slab(text: &str) {
4996 assert!(!text.is_empty());
4997 for cuts in header_straddle_cuts(text) {
4998 assert_cuts_match(text, &cuts);
4999 }
5000 let bare = text.trim_end_matches(['\n', '\r']);
5001 if !bare.is_empty() {
5002 assert_cuts_match(bare, &equal_cuts(bare.len(), 4));
5003 let mid = bare.len() / 2;
5004 let line_start = bare[..mid].rfind('\n').map(|i| i + 1).unwrap_or(0);
5005 let line_end = bare[mid..]
5006 .find('\n')
5007 .map(|i| mid + i)
5008 .unwrap_or(bare.len());
5009 if line_end - line_start > 1 {
5010 let cut = line_start + (line_end - line_start) / 2;
5011 assert_cuts_match(bare, &[0, cut, bare.len()]);
5012 }
5013 }
5014 let tailed = format!("{text}\n \n\t\n");
5015 assert_cuts_match(
5016 &tailed,
5017 &[0, 3, tailed.len() / 2, tailed.len() - 1, tailed.len()],
5018 );
5019 let crlf = text.replace('\n', "\r\n");
5020 assert_cuts_match(&crlf, &equal_cuts(crlf.len(), 5));
5021 assert_crlf_gap_cut(&crlf);
5022 let serial = frame_start_offsets_serial(text);
5023 if serial.len() >= 2 {
5024 let second = serial[1];
5025 let next = serial.get(2).copied().unwrap_or(text.len());
5026 let broken_at = second + (next - second) / 2;
5027 let broken = &text[..broken_at.max(1)];
5028 assert_cuts_match(broken, &[0, 1.max(broken.len() / 3), broken.len()]);
5029 }
5030 }
5031
5032 #[test]
5033 fn chunked_header_scan_matches_serial_offsets() {
5034 assert_eq!(frame_start_offsets_serial(""), frame_start_offsets(""));
5035 assert_cuts_match("\n", &[0, 1]);
5036 assert_cuts_match(" \n\n", &[0, 2, 5]);
5037 for name in [
5038 "tiny_cuh2.con",
5039 "tiny_cuh2.convel",
5040 "tiny_cuh2_forces.con",
5041 "tiny_cuh2_vel_forces.con",
5042 "cuh2.con",
5043 ] {
5044 let one = fixture(name);
5045 assert_slab(&one);
5046 assert_slab(&one.repeat(3));
5047 }
5048 let one = fixture("cuh2.con");
5052 for wide in [one.clone(), one.repeat(8)] {
5053 let serial = frame_start_offsets_serial(&wide);
5054 for workers in [2usize, 3, 4, 8] {
5055 let parallel = frame_start_offsets_with_threads(&wide, Some(workers));
5056 assert_eq!(parallel, serial, "workers={workers} bytes={}", wide.len());
5057 let parsed =
5058 frames_from_bytes(wide.as_bytes(), Some(workers)).expect("bytes parse");
5059 assert_eq!(parsed.len(), serial.len());
5060 let seq = frames_from_text(&wide, Some(1)).expect("serial parse");
5061 assert_eq!(parsed.len(), seq.len());
5062 assert_eq!(parsed[0].atom_data[0].x(), seq[0].atom_data[0].x());
5063 assert_eq!(
5064 parsed.last().unwrap().atom_data.last().unwrap().z(),
5065 seq.last().unwrap().atom_data.last().unwrap().z()
5066 );
5067 }
5068 }
5069 }
5070}
5071
5072#[cfg(all(test, feature = "parallel"))]
5077fn header_window_end(bytes: &[u8], start: usize, limit: usize) -> usize {
5078 let limit = limit.min(bytes.len());
5079 let marker = b"Coordinates";
5080 let mut line = start;
5081 while line < limit {
5082 let rest = &bytes[line..limit];
5083 let rel = memchr::memchr(b'\n', rest).unwrap_or(rest.len());
5084 let content_end = line + rel;
5085 let content = &bytes[line..content_end];
5086 let trimmed = match content.iter().position(|byte| !byte.is_ascii_whitespace()) {
5087 Some(index) => &content[index..],
5088 None => b"",
5089 };
5090 if trimmed.starts_with(marker) {
5091 return line;
5092 }
5093 if content_end >= limit {
5094 break;
5095 }
5096 line = content_end + 1;
5097 }
5098 limit
5099}
5100
5101#[cfg(all(test, feature = "parallel"))]
5102mod parallel_frame_parse_tests {
5103 use super::*;
5104 use std::path::PathBuf;
5105
5106 const ZERO_ATOM: &str = "\
5107zero-atom frame
5108{\"con_spec_version\":3,\"con_spec_minor\":1,\"units\":{\"length\":\"angstrom\",\"energy\":\"eV\"}}
510910.0 10.0 10.0
511090.0 90.0 90.0
5111
5112
51130
5114
5115
5116";
5117
5118 fn fixture(name: &str) -> String {
5119 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
5120 .join("resources/test")
5121 .join(name);
5122 std::fs::read_to_string(path).expect(name)
5123 }
5124
5125 fn serial_results(text: &str) -> Vec<Result<types::ConFrame, error::ParseError>> {
5126 ConFrameIterator::new(text).collect()
5127 }
5128
5129 fn written(frame: &types::ConFrame) -> Vec<u8> {
5130 let mut out = Vec::new();
5131 {
5132 let mut writer = crate::writer::ConFrameWriter::new(&mut out);
5133 writer.write_frame(frame).expect("write");
5134 writer.flush().expect("flush");
5135 }
5136 out
5137 }
5138
5139 fn assert_same_results(
5140 label: &str,
5141 serial: Vec<Result<types::ConFrame, error::ParseError>>,
5142 parallel: Vec<Result<types::ConFrame, error::ParseError>>,
5143 ) {
5144 assert_eq!(serial.len(), parallel.len(), "{label} count");
5145 for (i, (left, right)) in serial.into_iter().zip(parallel).enumerate() {
5146 match (left, right) {
5147 (Ok(a), Ok(b)) => {
5148 assert_eq!(written(&a), written(&b), "{label} bytes frame {i}");
5149 assert_eq!(a, b, "{label} fields frame {i}");
5150 }
5151 (Err(a), Err(b)) => {
5152 assert_eq!(
5153 format!("{a:?}"),
5154 format!("{b:?}"),
5155 "{label} error frame {i}"
5156 );
5157 }
5158 (Ok(_), Err(err)) => panic!("{label} frame {i} serial ok parallel {err:?}"),
5159 (Err(err), Ok(_)) => panic!("{label} frame {i} serial {err:?} parallel ok"),
5160 }
5161 }
5162 }
5163
5164 fn assert_workers(label: &str, text: &str) {
5165 let serial_ok = serial_results(text).iter().all(|item| item.is_ok());
5166 for workers in [1usize, 2, 3, 4, 8] {
5167 let parallel = parse_frames_parallel_with_threads(text, Some(workers));
5168 assert_same_results(
5169 &format!("{label} workers {workers}"),
5170 serial_results(text),
5171 parallel,
5172 );
5173 let bounds = collect_frame_starts(ConFrameIterator::new(text), true);
5174 let at = parse_frames_at_offsets(text, &bounds, Some(workers));
5175 assert_same_results(
5176 &format!("{label} offsets workers {workers}"),
5177 serial_results(text),
5178 at,
5179 );
5180 }
5181 if serial_ok {
5182 for workers in [2usize, 3, 8] {
5183 let bytes = frames_from_bytes(text.as_bytes(), Some(workers)).expect(label);
5184 let seq = frames_from_text(text, Some(1)).expect(label);
5185 assert_eq!(bytes.len(), seq.len(), "{label} byte count");
5186 for (i, (a, b)) in bytes.iter().zip(seq.iter()).enumerate() {
5187 assert_eq!(written(a), written(b), "{label} byte path frame {i}");
5188 assert_eq!(a, b, "{label} byte path fields frame {i}");
5189 }
5190 }
5191 }
5192 }
5193
5194 fn crlf_gap_offsets(text: &str) -> Vec<usize> {
5195 let bytes = text.as_bytes();
5196 let mut cut = None;
5197 let mut i = 0usize;
5198 while i + 1 < bytes.len() {
5199 if bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
5200 cut = Some(i + 1);
5201 break;
5202 }
5203 i += 1;
5204 }
5205 let cut = cut.expect("crlf");
5206 frame_start_offsets_at_cuts(text, &[0, cut, bytes.len()])
5207 }
5208
5209 #[test]
5210 fn parallel_slots_match_serial_frames() {
5211 assert_workers("empty", "");
5212 assert_workers("blank", "\n");
5213 assert_workers("space", " \n\n");
5214 assert_workers("zero", ZERO_ATOM);
5215 assert_workers("zero-repeat", &ZERO_ATOM.repeat(5));
5216 for name in [
5217 "tiny_cuh2.con",
5218 "tiny_cuh2.convel",
5219 "tiny_cuh2_forces.con",
5220 "tiny_cuh2_vel_forces.con",
5221 "tiny_cuh2_charges_spins_magmoms.con",
5222 "tiny_cuh2_displacements.con",
5223 "tiny_cuh2_spreads.con",
5224 "cuh2.con",
5225 ] {
5226 let one = fixture(name);
5227 assert_workers(name, &one);
5228 assert_workers(&format!("{name} x5"), &one.repeat(5));
5229 let bare = one.trim_end_matches(['\n', '\r']);
5230 assert_workers(&format!("{name} bare"), bare);
5231 let tailed = format!("{one}\n \n\t\n");
5232 assert_workers(&format!("{name} tail"), &tailed);
5233 let crlf = one.replace('\n', "\r\n");
5234 assert_workers(&format!("{name} crlf"), &crlf);
5235 let gap = crlf_gap_offsets(&crlf);
5236 assert_eq!(
5237 gap,
5238 frame_start_offsets_serial(&crlf),
5239 "{name} crlf gap offsets"
5240 );
5241 let parsed = parse_frames_at_offsets(&crlf, &gap, Some(4));
5242 assert_same_results(
5243 &format!("{name} crlf gap parse"),
5244 serial_results(&crlf),
5245 parsed,
5246 );
5247 let serial_offs = frame_start_offsets_serial(&one);
5248 let broken: &str = if serial_offs.len() >= 2 {
5249 let second = serial_offs[1];
5250 let next = serial_offs.get(2).copied().unwrap_or(one.len());
5251 &one[..second + (next - second) / 2]
5252 } else if one.len() > 8 {
5253 &one[..one.len() / 2]
5254 } else {
5255 ""
5256 };
5257 if !broken.is_empty() {
5258 let serial_err = frames_from_text(broken, Some(1))
5259 .err()
5260 .map(|e| format!("{e:?}"));
5261 assert!(serial_err.is_some(), "{name} chopped still parsed");
5262 for workers in [1usize, 2, 4, 8] {
5263 let parallel_err = frames_from_text(broken, Some(workers))
5264 .err()
5265 .map(|e| format!("{e:?}"));
5266 assert_eq!(parallel_err, serial_err, "{name} chopped workers {workers}");
5267 let byte_err = frames_from_bytes(broken.as_bytes(), Some(workers))
5268 .err()
5269 .map(|e| format!("{e:?}"));
5270 assert_eq!(
5271 byte_err, serial_err,
5272 "{name} chopped bytes workers {workers}"
5273 );
5274 }
5275 }
5276 }
5277 }
5278}
5279
5280#[cfg(all(test, feature = "parallel"))]
5281mod phased_read_tests {
5282 use super::*;
5283 use std::path::PathBuf;
5284
5285 fn fixture(name: &str) -> String {
5286 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
5287 .join("resources/test")
5288 .join(name);
5289 std::fs::read_to_string(path).expect(name)
5290 }
5291
5292 #[test]
5293 fn phased_bytes_match_serial_frames() {
5294 let text = fixture("cuh2.con").repeat(4);
5295 let serial = frames_from_text(&text, Some(1)).expect("serial");
5296 for workers in [1usize, 2, 4, 8] {
5297 let phased = frames_from_bytes_phased(text.as_bytes(), Some(workers)).expect("phased");
5298 let parallel = phased.frames;
5299 assert_eq!(parallel.len(), serial.len(), "workers={workers}");
5300 assert!(
5301 phased.scan_s >= 0.0 && phased.slot_alloc_s >= 0.0 && phased.worker_parse_s >= 0.0
5302 );
5303 assert_eq!(parallel[0], serial[0], "workers={workers} first");
5304 assert_eq!(
5305 parallel.last().unwrap(),
5306 serial.last().unwrap(),
5307 "workers={workers} last"
5308 );
5309 let slab = pack_position_slab(&serial, workers).expect("slab");
5310 let stride = serial[0].positions.nrows() * 3;
5311 let src = serial[0].positions.f64_slice().expect("f64");
5312 assert_eq!(&slab[..stride], src);
5313 let (chunked, _) = measure_newline_scan(&text, workers.max(2), true);
5314 let (plain, _) = measure_newline_scan(&text, 1, false);
5315 if workers >= 2 {
5316 assert_eq!(chunked, plain);
5317 }
5318 }
5319 }
5320
5321 #[test]
5322 fn phased_file_matches_serial_and_maps_a_wide_file() {
5323 let text = fixture("cuh2.con").repeat(5);
5324 let dir = std::env::temp_dir();
5325 let path = dir.join("readcon-phased-wide.con");
5326 std::fs::write(&path, &text).expect("write");
5327 let phased = read_all_frames_phased(&path, Some(4)).expect("phased");
5328 let serial = frames_from_text(&text, Some(1)).expect("serial");
5329 assert_eq!(phased.frames, serial);
5330 assert!(phased.phases.mapped, "a file past 64 KiB is mapped");
5331 assert!(phased.phases.worker_parse_s >= 0.0);
5332 assert_eq!(phased.n_bytes, text.len());
5333 let _ = std::fs::remove_file(&path);
5334 }
5335
5336 #[test]
5337 fn equal_span_boundaries_match_the_serial_walk() {
5338 let text = fixture("cuh2.con").repeat(8);
5339 let bytes = text.as_bytes();
5340 let got = equal_span_boundaries(bytes, 4).expect("equal span");
5341 let expect = collect_frame_starts(ConFrameIterator::from_bytes(bytes), true);
5342 assert_eq!(got, expect);
5343 assert_eq!(got.len(), 8);
5344 let phased = frames_from_bytes_phased(bytes, Some(4)).expect("phased");
5345 assert_eq!(phased.scan_kind, SCAN_EQUAL_SPAN);
5346 let wide = fixture("cuh2.con").repeat(32);
5347 let ranges = frames_from_bytes_phased(wide.as_bytes(), Some(4)).expect("ranges");
5348 assert_eq!(ranges.frames.len(), 32);
5349 assert_eq!(
5350 ranges.chunk_s.len(),
5351 4,
5352 "equal frames are one range per worker, so the shell stash is one burst"
5353 );
5354 assert_eq!(
5355 ranges.frames,
5356 frames_from_text(&wide, Some(1)).expect("wide serial")
5357 );
5358 assert_eq!(
5359 phased.frames,
5360 frames_from_text(&text, Some(1)).expect("serial")
5361 );
5362 let tailed = format!("{text}\n\n \n");
5363 let tail_phased = frames_from_bytes_phased(tailed.as_bytes(), Some(4)).expect("tail");
5364 assert_eq!(tail_phased.scan_kind, SCAN_EQUAL_SPAN);
5365 assert_eq!(
5366 tail_phased.frames,
5367 frames_from_text(&tailed, Some(1)).expect("tail serial")
5368 );
5369 }
5370
5371 #[test]
5372 fn a_wide_frame_matches_the_serial_parse() {
5373 let n = 5000usize;
5374 let mut text = String::new();
5375 text.push_str(
5376 "grid\n{\"con_spec_version\":2}\n10.0 10.0 10.0\n90.0 90.0 90.0\n0 0\n0 0\n1\n",
5377 );
5378 text.push_str(&format!("{n}\n63.546\nCu\nCoordinates of Component 1\n"));
5379 for i in 0..n {
5380 text.push_str(&format!("{i}.125000 0.250000 0.500000 0 {i}\n"));
5381 }
5382 let serial = frames_from_text(&text, Some(1)).expect("serial");
5383 let parallel = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("parallel");
5384 assert_eq!(parallel.frames, serial);
5385 }
5386
5387 fn one_species_frame(n: usize) -> String {
5388 let mut text = String::new();
5389 text.push_str(
5390 "grid\n{\"con_spec_version\":2}\n10.0 10.0 10.0\n90.0 90.0 90.0\n0 0\n0 0\n1\n",
5391 );
5392 text.push_str(&format!("{n}\n63.546\nCu\nCoordinates of Component 1\n"));
5393 for i in 0..n {
5394 text.push_str(&format!("{i}.125000 0.250000 0.500000 0 {i}\n"));
5395 }
5396 text
5397 }
5398
5399 fn two_species_frame(n_cu: usize, n_h: usize) -> String {
5400 let mut text = String::new();
5401 text.push_str(
5402 "grid\n{\"con_spec_version\":2}\n10.0 10.0 10.0\n90.0 90.0 90.0\n0 0\n0 0\n2\n",
5403 );
5404 text.push_str(&format!("{n_cu} {n_h}\n63.546 1.008\n"));
5405 text.push_str("Cu\nCoordinates of Component 1\n");
5406 for i in 0..n_cu {
5407 text.push_str(&format!("{i}.125000 0.250000 0.500000 0 {i}\n"));
5408 }
5409 text.push_str("H\nCoordinates of Component 2\n");
5410 for i in 0..n_h {
5411 text.push_str(&format!("1.500000 2.250000 {i}.125000 0 {}\n", n_cu + i));
5412 }
5413 text
5414 }
5415
5416 fn strip_gzip_row_table(bytes: &mut Vec<u8>) {
5421 assert_eq!(&bytes[0..2], b"\x1f\x8b");
5422 let xlen = u16::from_le_bytes([bytes[10], bytes[11]]) as usize;
5423 assert_eq!(&bytes[12..14], b"RC");
5424 let sub = u16::from_le_bytes([bytes[14], bytes[15]]) as usize;
5425 assert!(bytes[16..].starts_with(b"RCZ1"));
5426 let count = u16::from_le_bytes([bytes[20], bytes[21]]) as usize;
5427 let row_bytes = 8 * count;
5428 let payload_at = 16usize;
5429 assert_eq!(sub, xlen - 4);
5430 assert!(sub >= 6 + 16 * count + row_bytes);
5431 let row_at = payload_at + sub - row_bytes;
5432 bytes.drain(row_at..row_at + row_bytes);
5433 let new_sub = (sub - row_bytes) as u16;
5434 let new_xlen = (xlen - row_bytes) as u16;
5435 bytes[10..12].copy_from_slice(&new_xlen.to_le_bytes());
5436 bytes[14..16].copy_from_slice(&new_sub.to_le_bytes());
5437 for index in 1..count {
5438 let at = payload_at + 6 + 8 * index;
5439 let start = u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap());
5440 let start = start - row_bytes as u64;
5441 bytes[at..at + 8].copy_from_slice(&start.to_le_bytes());
5442 }
5443 }
5444
5445 #[test]
5446 fn indexed_gzip_read_matches_the_serial_frame() {
5447 let text = one_species_frame(5000);
5448 let serial = frames_from_text(&text, Some(1)).expect("serial");
5449 let dir = tempfile::tempdir().unwrap();
5450 let path = dir.path().join("wide.con.gz");
5451 let members = crate::compression::write_gzip_members_n(&path, text.as_bytes(), 4).unwrap();
5452 assert!(members > 1);
5453 let piped = read_all_frames_phased(&path, Some(4)).expect("piped");
5454 assert_eq!(piped.frames, serial);
5455 let mixed = two_species_frame(3000, 2000);
5456 let mixed_serial = frames_from_text(&mixed, Some(1)).expect("mixed serial");
5457 let mixed_path = dir.path().join("mixed.con.gz");
5458 crate::compression::write_gzip_members_n(&mixed_path, mixed.as_bytes(), 4).unwrap();
5459 let mixed_read = read_all_frames_phased(&mixed_path, Some(4)).expect("mixed");
5460 assert_eq!(mixed_read.frames, mixed_serial);
5461 let mut old = std::fs::read(&path).unwrap();
5462 strip_gzip_row_table(&mut old);
5463 let old_path = dir.path().join("old.con.gz");
5464 std::fs::write(&old_path, &old).unwrap();
5465 let fallback = read_all_frames_phased(&old_path, Some(4)).expect("old index");
5466 assert_eq!(fallback.frames, serial);
5467 }
5468
5469 #[cfg(feature = "zstd")]
5470 #[test]
5471 fn indexed_zstd_read_matches_the_serial_frame() {
5472 let text = one_species_frame(5000);
5473 let serial = frames_from_text(&text, Some(1)).expect("serial");
5474 let dir = tempfile::tempdir().unwrap();
5475 let path = dir.path().join("wide.con.zst");
5476 let members = crate::compression::write_zstd_members_n(&path, text.as_bytes(), 4).unwrap();
5477 assert!(members > 1);
5478 let piped = read_all_frames_phased(&path, Some(4)).expect("piped");
5479 assert_eq!(piped.frames, serial);
5480 }
5481
5482 #[test]
5483 fn unequal_byte_spans_use_the_indexed_header_walk() {
5484 let one = fixture("cuh2.con");
5485 let mut longer = one.clone();
5486 longer.insert_str(0, "pad ");
5487 let text = format!("{one}{longer}{one}");
5488 let serial = frames_from_text(&text, Some(1)).expect("serial");
5489 let phased = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("phased");
5490 assert_eq!(phased.scan_kind, SCAN_INDEXED);
5491 assert_eq!(phased.frames, serial);
5492 assert!(equal_span_boundaries(text.as_bytes(), 4).is_none());
5493 }
5494
5495 #[test]
5496 fn a_whitespace_tail_uses_the_equal_span_walk() {
5497 let mut text = fixture("cuh2.con").repeat(3);
5498 text.push('\n');
5499 assert!(equal_span_boundaries(text.as_bytes(), 4).is_some());
5500 let serial = frames_from_text(&text, Some(1)).expect("serial");
5501 let parallel = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("phased");
5502 assert_eq!(parallel.scan_kind, SCAN_EQUAL_SPAN);
5503 assert_eq!(parallel.frames, serial);
5504 }
5505
5506 #[test]
5507 fn touch_checksum_matches_across_worker_counts() {
5508 let text = fixture("cuh2.con").repeat(4);
5509 let serial = frames_from_bytes_phased(text.as_bytes(), Some(1)).expect("serial");
5510 let parallel = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("parallel");
5511 let slab_s = pack_position_slab(&serial.frames, 1).expect("slab");
5512 let slab_p = pack_position_slab(¶llel.frames, 4).expect("slab");
5513 let left = touch_frame_checksum(&serial.frames, &slab_s, 1).expect("touch");
5514 let right = touch_frame_checksum(¶llel.frames, &slab_p, 4).expect("touch");
5515 assert_eq!(left, right);
5516 assert_ne!(left, 0);
5517 }
5518}
5519
5520#[cfg(test)]
5521mod later_frame_location_tests {
5522 use super::*;
5523 use std::fs;
5524
5525 fn one_atom(coord: &str) -> String {
5526 format!(
5527 "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"
5528 )
5529 }
5530
5531 fn bad_count_frame() -> String {
5532 "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()
5533 }
5534
5535 fn two_frames() -> String {
5536 format!("{}{}", one_atom("0 0 0 0 0"), bad_count_frame())
5537 }
5538
5539 fn assert_later_count(
5540 err: &error::ParseError,
5541 line: u32,
5542 frame: u64,
5543 iter: &ConFrameIterator<'_>,
5544 ) {
5545 assert_eq!(err.class_name(), "InvalidCount", "{err}");
5546 assert_eq!(iter.lines.line_no(), line, "{err}");
5547 assert_eq!(iter.frame_index(), frame, "{err}");
5548 }
5549
5550 fn assert_first_pass(err: Box<dyn std::error::Error>) {
5551 let located = err
5552 .downcast_ref::<error::LocatedParseError>()
5553 .unwrap_or_else(|| panic!("first pass lost the location: {err}"));
5554 assert_eq!(located.error.class_name(), "InvalidCount", "{located:?}");
5555 assert_eq!(located.line, 20, "{located:?}");
5556 assert_eq!(located.frame_index, 1, "{located:?}");
5557 }
5558
5559 fn take_err<T>(result: Result<T, Box<dyn std::error::Error>>) -> Box<dyn std::error::Error> {
5560 match result {
5561 Err(err) => err,
5562 Ok(_) => panic!("expected a parse error"),
5563 }
5564 }
5565
5566 #[test]
5569 fn batch_readers_keep_the_later_frame_on_the_first_pass() {
5570 let path = std::env::temp_dir().join(format!(
5571 "readcon-later-frame-first-{}.con",
5572 std::process::id()
5573 ));
5574 fs::write(&path, two_frames()).unwrap();
5575 for threads in [None, Some(1), Some(2)] {
5576 assert_first_pass(take_err(read_all_frames_with_threads(&path, threads)));
5577 assert_first_pass(take_err(read_all_frames_phased(&path, threads)));
5578 assert_first_pass(take_err(read_positions_with_threads(&path, threads)));
5579 }
5580 let _ = fs::remove_file(&path);
5581 }
5582
5583 #[test]
5585 fn next_forward_and_skip_locate_the_later_frame() {
5586 let text = two_frames();
5587
5588 let mut iter = ConFrameIterator::new(&text);
5589 assert!(iter.next().unwrap().is_ok());
5590 let err = iter.next().unwrap().unwrap_err();
5591 assert_later_count(&err, 20, 1, &iter);
5592
5593 let mut iter = ConFrameIterator::new(&text);
5594 assert!(iter.forward().unwrap().is_ok());
5595 let err = iter.forward().unwrap().unwrap_err();
5596 assert_later_count(&err, 20, 1, &iter);
5597
5598 let mut iter = ConFrameIterator::new(&text);
5599 let err = iter.skip_frames(3).unwrap_err();
5600 assert_later_count(&err, 20, 1, &iter);
5601 }
5602
5603 #[test]
5607 fn skip_after_a_velocity_frame_keeps_the_separator_line() {
5608 let text = format!(
5609 "{}\nH\nVelocities of Component 1\n0 0 0 0 0\n{}",
5610 one_atom("0 0 0 0 0"),
5611 bad_count_frame()
5612 );
5613 let mut iter = ConFrameIterator::new(&text);
5614 assert!(iter.forward().unwrap().is_ok());
5615 let err = iter.forward().unwrap().unwrap_err();
5616 assert_later_count(&err, 24, 1, &iter);
5617 }
5618
5619 #[test]
5620 fn count_frames_locates_the_later_frame() {
5621 let path = std::env::temp_dir().join(format!(
5622 "readcon-later-frame-count-{}.con",
5623 std::process::id()
5624 ));
5625 fs::write(&path, two_frames()).unwrap();
5626 let err = count_frames(&path).unwrap_err();
5627 let _ = fs::remove_file(&path);
5628 match err {
5629 LoadError::Parse(located) => {
5630 assert_eq!(located.error.class_name(), "InvalidCount");
5631 assert_eq!(located.line, 20, "{located}");
5632 assert_eq!(located.frame_index, 1, "{located}");
5633 }
5634 other => panic!("expected a located parse error, got {other}"),
5635 }
5636 }
5637
5638 #[test]
5641 fn metadata_in_a_later_frame_reports_that_frames_line() {
5642 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";
5643 let text = format!("{}{}", one_atom("0 0 0 0 0"), bad);
5644 for threads in [Some(1usize), Some(2)] {
5645 let err = frames_from_text_located(&text, threads).unwrap_err();
5646 assert_eq!(
5647 err.error.class_name(),
5648 "InvalidMetadataJson",
5649 "{threads:?}: {err}"
5650 );
5651 assert_eq!(err.frame_index, 1, "{threads:?}");
5652 assert_eq!(err.line, 14, "{threads:?}: {err}");
5653 }
5654 }
5655
5656 #[test]
5664 fn parallel_positions_report_the_earliest_failing_slot() {
5665 const SLOW_ATOMS: usize = 20_000;
5666 let slow = counted_frame(SLOW_ATOMS);
5667 let bad = counted_frame(1);
5668 let text = format!("{slow}{bad}{bad}{bad}");
5669 let earliest_line = 1 + slow.bytes().filter(|byte| *byte == b'\n').count() as u32;
5670 let path =
5671 std::env::temp_dir().join(format!("readcon-earliest-slot-{}.con", std::process::id()));
5672 fs::write(&path, &text).unwrap();
5673 assert_earliest_slot(
5674 take_err(read_positions_with_threads(&path, Some(1))),
5675 earliest_line,
5676 );
5677 assert_earliest_slot(
5678 take_err(read_positions_with_threads(&path, Some(2))),
5679 earliest_line,
5680 );
5681 let _ = fs::remove_file(&path);
5682 }
5683
5684 fn counted_frame(natoms: usize) -> String {
5685 let mut body = String::with_capacity(natoms * 10 + 96);
5686 body.push_str("seed\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\n0 0\n0 0 0\n1\n");
5687 body.push_str(&natoms.to_string());
5688 body.push_str("\n1.0\nH\nCoordinates of Component 1\n");
5689 for _ in 0..natoms {
5690 body.push_str("0 0 0 0 0\n");
5691 }
5692 body
5693 }
5694
5695 fn assert_earliest_slot(err: Box<dyn std::error::Error>, line: u32) {
5696 let located = err
5697 .downcast_ref::<error::LocatedParseError>()
5698 .unwrap_or_else(|| panic!("position read lost the location: {err}"));
5699 assert_eq!(located.error.class_name(), "InvalidCount", "{located:?}");
5700 assert_eq!(located.frame_index, 1, "{located:?}");
5701 assert_eq!(located.line, line, "{located:?}");
5702 }
5703}
5704
5705#[cfg(test)]
5706mod frame_equality_tests {
5707 use super::ConFrameIterator;
5708
5709 fn sample_con(frames: usize, long: bool) -> String {
5710 let mut out = String::new();
5711 for f in 0..frames {
5712 let shift = (f as f64) * 1.0e-4;
5713 out.push_str("Generated by benchmark\n");
5714 out.push_str("{\"con_spec_version\":2}\n");
5715 out.push_str("15.3456 21.702 100.0\n");
5716 out.push_str("90.0 90.0 90.0\n");
5717 out.push_str("0 0\n");
5718 out.push_str(&format!("{f} 0 1\n"));
5719 out.push_str("2\n6 2\n63.546 1.00793\n");
5720 out.push_str("Cu\nCoordinates of Component 1\n");
5721 for i in 0..6 {
5722 let x = (i as f64) * 2.557 + 0.6394 + shift;
5723 let y = 0.9045 + shift * 0.5;
5724 let z = (f as f64) * 1.0e-6 + if i == 3 { -1.25 } else { 0.0 };
5725 if long {
5726 out.push_str(&format!(" {x:.17} {y:.17} {z:.17} 0 {i}\n"));
5727 } else {
5728 out.push_str(&format!(" {x:.6} {y:.6} {z:.6} 0 {i}\n"));
5729 }
5730 }
5731 out.push_str("H\nCoordinates of Component 2\n");
5732 out.push_str(" 8.6823 9.947 11.733 0 6\n");
5733 out.push_str(&format!(" -7.9421 9.947 {shift:.6} 0 7\n"));
5734 }
5735 out
5736 }
5737
5738 fn sample_convel() -> String {
5739 let mut out = String::new();
5740 for f in 0..3 {
5741 let shift = (f as f64) * 1.0e-4;
5742 out.push_str("Generated by benchmark\n");
5743 out.push_str("{\"con_spec_version\":2}\n");
5744 out.push_str("15.3456 21.702 100.0\n");
5745 out.push_str("90.0 90.0 90.0\n");
5746 out.push_str("0 0\n");
5747 out.push_str(&format!("{f} 0 1\n"));
5748 out.push_str("2\n4 1\n63.546 1.00793\n");
5749 out.push_str("Cu\nCoordinates of Component 1\n");
5750 for i in 0..4 {
5751 let x = (i as f64) * 2.557 + shift;
5752 out.push_str(&format!(" {x:.17} 0.9045 1.0 0 {i}\n"));
5753 }
5754 out.push_str("H\nCoordinates of Component 2\n");
5755 out.push_str(" 1.5 2.5 3.5 0 4\n");
5756 out.push_str("\nCu\nVelocities of Component 1\n");
5757 for i in 0..4 {
5758 let vx = (i as f64) * 1.0e-4 - shift;
5759 out.push_str(&format!(" {vx:.17} -0.5 0.25 0 {i}\n"));
5760 }
5761 out.push_str("H\nVelocities of Component 2\n");
5762 out.push_str(" 0.1 -0.2 0.3 0 4\n");
5763 }
5764 out
5765 }
5766
5767 fn reference_f64(token: &str) -> f64 {
5768 let (val, n) = fast_float2::parse_partial::<f64, _>(token).expect(token);
5769 assert_eq!(n, token.len(), "{token}");
5770 val
5771 }
5772
5773 fn assert_rows_match(text: &str, positions: bool) {
5774 let frames: Vec<_> = ConFrameIterator::new(text)
5775 .map(|frame| frame.expect("parse"))
5776 .collect();
5777 assert!(frames.len() >= 2);
5778 let mut frame_i = 0usize;
5779 let mut row = 0usize;
5780 let mut in_section = false;
5781 for line in text.lines() {
5782 let token = line.trim();
5783 if token.is_empty() {
5784 in_section = true;
5785 row = 0;
5786 continue;
5787 }
5788 if token.starts_with("Generated") {
5789 if row > 0 || in_section {
5790 frame_i += 1;
5791 }
5792 row = 0;
5793 in_section = false;
5794 continue;
5795 }
5796 let cols: Vec<&str> = token.split_whitespace().collect();
5797 if cols.len() < 4 || reference_ok(cols[0]).is_none() {
5798 continue;
5799 }
5800 if cols[3].parse::<u64>().is_err() {
5801 continue;
5802 }
5803 let got = if in_section {
5804 frames[frame_i].atom_data[row].velocity().expect("velocity")
5805 } else if positions {
5806 let p = frames[frame_i].positions.as_f64_row(row);
5807 [p[0], p[1], p[2]]
5808 } else {
5809 continue;
5810 };
5811 for (k, token) in cols.iter().take(3).enumerate() {
5812 assert_eq!(
5813 got[k].to_bits(),
5814 reference_f64(token).to_bits(),
5815 "frame {frame_i} row {row} col {k} token {token} section {in_section}"
5816 );
5817 }
5818 let aos = if in_section {
5819 frames[frame_i].atom_data[row].velocity().unwrap()
5820 } else {
5821 let atom = &frames[frame_i].atom_data[row];
5822 [atom.x(), atom.y(), atom.z()]
5823 };
5824 for k in 0..3 {
5825 assert_eq!(aos[k].to_bits(), got[k].to_bits());
5826 }
5827 row += 1;
5828 }
5829 }
5830
5831 fn reference_ok(token: &str) -> Option<f64> {
5832 let (val, n) = fast_float2::parse_partial::<f64, _>(token).ok()?;
5833 (n == token.len()).then_some(val)
5834 }
5835
5836 #[test]
5837 fn frames_match_general_decimal_parser() {
5838 assert_rows_match(&sample_con(4, true), true);
5839 assert_rows_match(&sample_con(4, false), true);
5840 let text = sample_convel();
5841 let frames: Vec<_> = ConFrameIterator::new(&text)
5842 .map(|frame| frame.expect("convel"))
5843 .collect();
5844 assert_eq!(frames.len(), 3);
5845 assert!(
5846 frames
5847 .iter()
5848 .all(|frame| frame.atom_data[0].velocity().is_some())
5849 );
5850 assert_eq!(frames[0].positions.nrows(), 5);
5851 let mut frame_i = 0usize;
5853 let mut coord_row = 0usize;
5854 let mut vel_row = 0usize;
5855 let mut velocities = false;
5856 for line in text.lines() {
5857 let token = line.trim();
5858 if token.starts_with("Generated") && (coord_row > 0 || velocities) {
5859 frame_i += 1;
5860 coord_row = 0;
5861 vel_row = 0;
5862 velocities = false;
5863 }
5864 if token.starts_with("Velocities") {
5865 velocities = true;
5866 continue;
5867 }
5868 let cols: Vec<&str> = token.split_whitespace().collect();
5869 if cols.len() < 5 {
5870 continue;
5871 }
5872 if reference_ok(cols[0]).is_none() || cols[3].parse::<u64>().is_err() {
5873 continue;
5874 }
5875 let row = if velocities { vel_row } else { coord_row };
5876 let got = if velocities {
5877 frames[frame_i].atom_data[row].velocity().unwrap()
5878 } else {
5879 let p = frames[frame_i].positions.as_f64_row(row);
5880 [p[0], p[1], p[2]]
5881 };
5882 for (k, col) in cols.iter().take(3).enumerate() {
5883 assert_eq!(got[k].to_bits(), reference_f64(col).to_bits(), "{token}");
5884 }
5885 if velocities {
5886 let soa = frames[frame_i].velocities.as_f64_row(row);
5887 for k in 0..3 {
5888 assert_eq!(soa[k].to_bits(), got[k].to_bits());
5889 }
5890 vel_row += 1;
5891 } else {
5892 coord_row += 1;
5893 }
5894 }
5895 assert_eq!(frame_i, 2);
5896 assert_eq!(vel_row, 5);
5897 }
5898}
5899
5900#[cfg(test)]
5901mod newline_skip_tests {
5902 use super::ConFrameIterator;
5903
5904 fn naive(bytes: &[u8], n: usize) -> Result<usize, ()> {
5907 let mut pos = 0usize;
5908 for i in 0..n {
5909 let rest = &bytes[pos..];
5910 match memchr::memchr(b'\n', rest) {
5911 Some(at) => pos += at + 1,
5912 None => {
5913 if rest.is_empty() {
5914 return Err(());
5915 }
5916 pos = bytes.len();
5917 if i + 1 == n {
5918 return Ok(pos);
5919 }
5920 return Err(());
5921 }
5922 }
5923 }
5924 Ok(pos)
5925 }
5926
5927 #[test]
5928 fn counted_skip_matches_per_line_memchr() {
5929 let mut samples = vec![
5930 String::new(),
5931 "a".into(),
5932 "a\n".into(),
5933 "a\nb".into(),
5934 "\n".into(),
5935 "\n\n".into(),
5936 "abc\ndef\nghi".into(),
5937 "a\r\nb\r\n".into(),
5938 "x".repeat(100) + "\n" + &"y".repeat(40),
5939 ];
5940 let path =
5941 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/cuh2.con");
5942 let one = std::fs::read_to_string(&path).unwrap();
5943 samples.push(one.repeat(2));
5944 let bare = one.trim_end_matches(['\n', '\r']).to_string();
5945 samples.push(bare);
5946 for text in samples {
5947 let bytes = text.as_bytes();
5948 for n in [0usize, 1, 2, 3, 6, 7, 9, 40, 200, 450, 1000, 5000] {
5949 let expect = naive(bytes, n);
5950 let mut it = ConFrameIterator::from_bytes(bytes);
5951 let got = it.advance_lines(n).map(|_| it.byte_pos()).map_err(|_| ());
5952 assert_eq!(got, expect, "n={n} len={}", bytes.len());
5953 }
5954 }
5955 }
5956}
5957
5958#[cfg(test)]
5959mod cached_unit_table_read_paths {
5960 use super::*;
5961
5962 const FRAME: &str = "\
5963v3
5964{\"con_spec_version\":3,\"con_spec_minor\":1,\"units\":{\"length\":\"angstrom\",\"energy\":\"eV\"}}
596510 10 10
596690 90 90
5967
5968
59691
59701
59711.0
5972H
5973Coordinates of Component 1
59740.5 0.25 0.125 0 7
5975";
5976
5977 fn coords(frame: &types::ConFrame) -> [f64; 3] {
5978 frame.positions.as_f64_row(0)
5979 }
5980
5981 #[test]
5984 fn read_paths_share_the_cached_unit_table() {
5985 let addr = crate::units::cached_unit_table_addr();
5986 let serial = ConFrameIterator::new(FRAME)
5987 .next()
5988 .expect("frame")
5989 .expect("parse");
5990 let from_bytes = frames_from_bytes(FRAME.as_bytes(), Some(1)).expect("bytes");
5991 let phased = frames_from_bytes_phased(FRAME.as_bytes(), Some(1)).expect("phased");
5992 assert_eq!(from_bytes.len(), 1);
5993 assert_eq!(phased.frames.len(), 1);
5994 assert_eq!(coords(&from_bytes[0]), coords(&serial));
5995 assert_eq!(coords(&phased.frames[0]), coords(&serial));
5996 assert_eq!(crate::units::cached_unit_table_addr(), addr);
5997 let bad = FRAME.replace("angstrom", "not-a-unit");
5998 assert!(ConFrameIterator::new(&bad).next().unwrap().is_err());
5999 assert!(frames_from_bytes(bad.as_bytes(), Some(1)).is_err());
6000 assert!(frames_from_bytes_phased(bad.as_bytes(), Some(1)).is_err());
6001 assert_eq!(crate::units::cached_unit_table_addr(), addr);
6002 }
6003}