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