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 line_no: usize,
28}
29
30impl<'a> MemchrLines<'a> {
31 pub fn new(text: &'a str) -> Self {
32 Self::from_parts(text.as_bytes(), false)
33 }
34
35 pub fn from_bytes(bytes: &'a [u8]) -> Self {
41 Self::from_parts(bytes, true)
42 }
43
44 fn from_parts(bytes: &'a [u8], check_utf8: bool) -> Self {
45 Self {
46 bytes,
47 pos: 0,
48 peeked: None,
49 check_utf8,
50 utf8_bad: false,
51 newlines: None,
52 line_no: 0,
53 }
54 }
55
56 #[inline]
57 fn read_one(&mut self) -> Option<&'a str> {
58 if self.newlines.is_some() {
59 return self.read_one_indexed();
60 }
61 if self.utf8_bad || self.pos >= self.bytes.len() {
62 return None;
63 }
64 let rest = &self.bytes[self.pos..];
65 let (line_bytes, advance) = match memchr::memchr(b'\n', rest) {
66 Some(i) => (&rest[..i], i + 1),
67 None => (rest, rest.len()),
68 };
69 let trimmed = if line_bytes.last() == Some(&b'\r') {
70 &line_bytes[..line_bytes.len() - 1]
71 } else {
72 line_bytes
73 };
74 if self.check_utf8 {
75 match std::str::from_utf8(trimmed) {
76 Ok(s) => {
77 self.pos += advance;
78 Some(s)
79 }
80 Err(_) => {
81 self.utf8_bad = true;
84 None
85 }
86 }
87 } else {
88 self.pos += advance;
89 Some(unsafe { std::str::from_utf8_unchecked(trimmed) })
91 }
92 }
93
94 #[inline]
95 pub fn next_line(&mut self) -> Option<&'a str> {
96 if let Some(p) = self.peeked.take() {
97 return Some(p);
98 }
99 self.read_one()
100 }
101
102 #[inline]
103 pub fn peek_line(&mut self) -> Option<&'a str> {
104 if self.peeked.is_none() {
105 self.peeked = self.read_one();
106 }
107 self.peeked
108 }
109
110 fn clear_peek(&mut self) {
112 if let Some(p) = self.peeked.take() {
113 let start = p.as_ptr() as usize - self.bytes.as_ptr() as usize;
115 self.pos = start;
116 if let Some(nl) = self.newlines.as_deref() {
117 self.line_no = line_no_for_pos(nl, self.bytes.len(), start);
118 }
119 }
120 }
121
122 fn read_one_indexed(&mut self) -> Option<&'a str> {
128 if self.utf8_bad || self.pos >= self.bytes.len() {
129 return None;
130 }
131 let len = self.bytes.len();
132 let line_no = {
133 let nl = self.newlines.as_deref().unwrap();
134 if self.pos == line_start_pos(nl, len, self.line_no) {
135 self.line_no
136 } else {
137 line_no_for_pos(nl, len, self.pos)
138 }
139 };
140 let Some(span) = indexed_span(self.newlines.as_deref().unwrap(), self.bytes, line_no)
141 else {
142 return None;
143 };
144 let trimmed = &self.bytes[span.start..span.content_end];
145 if self.check_utf8 {
146 match std::str::from_utf8(trimmed) {
147 Ok(s) => {
148 self.pos = span.next_pos;
149 self.line_no = line_no + 1;
150 Some(s)
151 }
152 Err(_) => {
153 self.utf8_bad = true;
154 self.line_no = line_no;
155 None
156 }
157 }
158 } else {
159 self.pos = span.next_pos;
160 self.line_no = line_no + 1;
161 Some(unsafe { std::str::from_utf8_unchecked(trimmed) })
163 }
164 }
165
166 fn utf8_error(&self) -> Option<error::ParseError> {
167 self.utf8_bad.then(invalid_utf8)
168 }
169
170 fn prefer_utf8(&self, err: error::ParseError) -> error::ParseError {
172 if self.utf8_bad { invalid_utf8() } else { err }
173 }
174}
175
176fn invalid_utf8() -> error::ParseError {
177 error::ParseError::ValidationError("invalid UTF-8".into())
178}
179
180impl<'a> Iterator for MemchrLines<'a> {
181 type Item = &'a str;
182 fn next(&mut self) -> Option<&'a str> {
183 self.next_line()
184 }
185}
186
187impl<'a> LineStream<'a> for MemchrLines<'a> {
188 #[inline]
189 fn next_line(&mut self) -> Option<&'a str> {
190 MemchrLines::next_line(self)
191 }
192 #[inline]
193 fn peek_line(&mut self) -> Option<&'a str> {
194 MemchrLines::peek_line(self)
195 }
196 #[inline]
197 fn next_coord_row(
198 &mut self,
199 spec_version: u32,
200 default_id: u64,
201 ) -> Result<([f64; 3], [bool; 3], u64), crate::error::ParseError> {
202 if let Some(p) = self.peeked.take() {
203 return crate::parser::parse_coord_row_at(p.as_bytes(), 0, spec_version, default_id)
204 .map(|(row, _)| row);
205 }
206 let (row, pos) =
207 crate::parser::parse_coord_row_at(self.bytes, self.pos, spec_version, default_id)?;
208 self.pos = pos;
209 Ok(row)
210 }
211 fn remainder_is_whitespace(&mut self) -> bool {
212 if self.utf8_bad {
213 return false;
214 }
215 if let Some(line) = self.peek_line()
216 && !line.trim().is_empty()
217 {
218 return false;
219 }
220 if self.utf8_bad {
223 return false;
224 }
225 self.bytes[self.pos..]
226 .iter()
227 .all(|b| b.is_ascii_whitespace())
228 }
229 fn blank_line_opens_section(&mut self) -> bool {
230 let Some(peeked) = self.peeked else {
232 return false;
233 };
234 if !peeked.trim().is_empty() {
235 return false;
236 }
237 match probe_section(&self.bytes[self.pos..]) {
238 SectionProbe::Opens => true,
239 SectionProbe::Closed => false,
240 SectionProbe::InvalidUtf8 => {
243 self.utf8_bad = true;
244 false
245 }
246 }
247 }
248
249 fn blank_opens_velocity_block(&mut self) -> bool {
250 match self.peek_line() {
251 Some(line) if line.trim().is_empty() => self.blank_line_opens_section(),
252 _ => true,
253 }
254 }
255}
256
257fn split_raw_line(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
258 if bytes.is_empty() {
259 return None;
260 }
261 match memchr::memchr(b'\n', bytes) {
262 Some(i) => Some((&bytes[..i], &bytes[i + 1..])),
263 None => Some((bytes, &[])),
264 }
265}
266
267fn strip_cr(line: &[u8]) -> &[u8] {
268 if line.last() == Some(&b'\r') {
269 &line[..line.len() - 1]
270 } else {
271 line
272 }
273}
274
275enum SectionProbe {
276 Opens,
277 Closed,
278 InvalidUtf8,
279}
280
281fn probe_section(after_blank: &[u8]) -> SectionProbe {
284 let Some((raw1, after1)) = split_raw_line(after_blank) else {
285 return SectionProbe::Closed;
286 };
287 let line1 = strip_cr(raw1);
288 if line1.iter().all(|b| matches!(b, b' ' | b'\t')) {
289 return SectionProbe::Closed;
290 }
291 let Ok(text1) = std::str::from_utf8(line1) else {
292 return SectionProbe::InvalidUtf8;
293 };
294 if text1.trim_start().starts_with('{') {
295 return SectionProbe::Closed;
296 }
297 let Some((raw2, _)) = split_raw_line(after1) else {
298 return SectionProbe::Closed;
299 };
300 let Ok(text2) = std::str::from_utf8(strip_cr(raw2)) else {
301 return SectionProbe::InvalidUtf8;
302 };
303 if crate::parser::line_has_component_label(text2) {
304 SectionProbe::Opens
305 } else {
306 SectionProbe::Closed
307 }
308}
309
310fn blank_opens_section(after_blank: &[u8]) -> bool {
311 matches!(probe_section(after_blank), SectionProbe::Opens)
312}
313
314pub struct ConFrameIterator<'a> {
325 pub(crate) lines: MemchrLines<'a>,
326}
327
328impl<'a> ConFrameIterator<'a> {
329 pub fn new(file_contents: &'a str) -> Self {
335 ConFrameIterator {
336 lines: MemchrLines::new(file_contents),
337 }
338 }
339
340 pub(crate) fn starting_at(file_contents: &'a str, pos: usize) -> Self {
345 debug_assert!(pos == 0 || file_contents.as_bytes()[pos - 1] == b'\n');
346 let mut it = Self::new(file_contents);
347 it.lines.pos = pos;
348 it
349 }
350
351 pub fn from_bytes(bytes: &'a [u8]) -> Self {
357 ConFrameIterator {
358 lines: MemchrLines::from_bytes(bytes),
359 }
360 }
361
362 fn advance_lines(&mut self, n: usize) -> Result<(), error::ParseError> {
368 self.lines.clear_peek();
369 if self.lines.newlines.is_some() {
370 return self.advance_lines_indexed(n);
371 }
372 for i in 0..n {
373 let rest = &self.lines.bytes[self.lines.pos..];
374 match memchr::memchr(b'\n', rest) {
375 Some(pos) => self.lines.pos += pos + 1,
376 None => {
377 if rest.is_empty() {
378 return Err(error::ParseError::IncompleteFrame);
379 }
380 self.lines.pos = self.lines.bytes.len();
381 if i + 1 == n {
382 return Ok(());
383 }
384 return Err(error::ParseError::IncompleteFrame);
385 }
386 }
387 }
388 Ok(())
389 }
390
391 fn advance_lines_to(&mut self, n: usize, end: Option<usize>) -> Result<(), error::ParseError> {
397 if self.lines.newlines.is_some() {
400 return self.advance_lines(n);
401 }
402 self.lines.clear_peek();
403 let pos = self.lines.pos;
404 let bytes = self.lines.bytes;
405 if let Some(end) = end
406 && pos < end
407 && end <= bytes.len()
408 && bytes[end - 1] == b'\n'
409 && memchr::memchr_iter(b'\n', &bytes[pos..end]).count() == n
410 {
411 self.lines.pos = end;
412 return Ok(());
413 }
414 self.advance_lines(n)
415 }
416
417 fn advance_lines_indexed(&mut self, n: usize) -> Result<(), error::ParseError> {
422 let len = self.lines.bytes.len();
423 let step = {
424 let nl = self.lines.newlines.as_deref().unwrap();
425 let nlines = line_count(nl, len);
426 let line_no = if self.lines.pos == line_start_pos(nl, len, self.lines.line_no) {
427 self.lines.line_no
428 } else {
429 line_no_for_pos(nl, len, self.lines.pos)
430 };
431 if n == 0 {
432 IndexStep::Stay(line_no)
433 } else if line_no >= nlines {
434 IndexStep::Missing(line_no)
435 } else {
436 match line_no.checked_add(n) {
437 Some(target) if target <= nlines => {
438 let pos = if target == nlines {
439 len
440 } else {
441 line_start_pos(nl, len, target)
442 };
443 IndexStep::Landed(target, pos)
444 }
445 _ => IndexStep::Short(nlines, len),
446 }
447 }
448 };
449 match step {
450 IndexStep::Stay(line_no) => {
451 self.lines.line_no = line_no;
452 Ok(())
453 }
454 IndexStep::Missing(line_no) => {
455 self.lines.line_no = line_no;
456 Err(error::ParseError::IncompleteFrame)
457 }
458 IndexStep::Landed(line_no, pos) => {
459 self.lines.line_no = line_no;
460 self.lines.pos = pos;
461 Ok(())
462 }
463 IndexStep::Short(line_no, pos) => {
464 self.lines.line_no = line_no;
465 self.lines.pos = pos;
466 Err(error::ParseError::IncompleteFrame)
467 }
468 }
469 }
470
471 fn read_line_str(&mut self) -> Option<&'a str> {
473 self.lines.clear_peek();
474 self.lines.next_line()
475 }
476
477 pub fn forward_fast(&mut self) -> Option<Result<(), error::ParseError>> {
481 self.forward_fast_to(None)
482 }
483
484 pub(crate) fn forward_fast_to(
489 &mut self,
490 end: Option<usize>,
491 ) -> Option<Result<(), error::ParseError>> {
492 self.lines.clear_peek();
493 if self.lines.pos >= self.lines.bytes.len() {
494 return None;
495 }
496 if let Err(e) = self.advance_lines(6) {
498 return Some(Err(e));
499 }
500 let natm_types: usize = match self.read_line_str() {
502 Some(line) => match crate::parser::parse_count_line(line, 1) {
503 Ok(v) => v[0],
504 Err(e) => return Some(Err(e)),
505 },
506 None => return Some(Err(error::ParseError::IncompleteHeader)),
507 };
508 if natm_types == 0 {
509 return Some(crate::parser::read_zero_atom_tail(|| self.read_line_str()));
512 }
513 let natms_per_type: Vec<usize> = match self.read_line_str() {
515 Some(line) => match crate::parser::parse_count_line(line, natm_types) {
516 Ok(v) => v,
517 Err(e) => return Some(Err(e)),
518 },
519 None => return Some(Err(error::ParseError::IncompleteHeader)),
520 };
521 if natms_per_type.iter().any(|n| *n == 0) {
522 return Some(Err(error::ParseError::InvalidCount(
523 "each per-type count must be >= 1".into(),
524 )));
525 }
526 if let Err(e) = self.advance_lines(1) {
528 return Some(Err(e));
529 }
530 let total_atoms: usize = natms_per_type.iter().sum();
531 let coord_block_lines = total_atoms + natm_types * 2;
532 if let Err(e) = self.advance_lines_to(coord_block_lines, end) {
533 return Some(Err(e));
534 }
535 self.lines.clear_peek();
537 loop {
538 let rest = &self.lines.bytes[self.lines.pos..];
539 if rest.is_empty() {
540 break;
541 }
542 let next_eol = memchr::memchr(b'\n', rest);
543 let line = match next_eol {
544 Some(pos) => &rest[..pos],
545 None => rest,
546 };
547 let is_blank = line.iter().all(|b| matches!(b, b' ' | b'\t' | b'\r'));
548 if !is_blank {
549 break;
550 }
551 let after = match next_eol {
552 Some(pos) => &rest[pos + 1..],
553 None => &[][..],
554 };
555 if after.iter().all(|b| b.is_ascii_whitespace()) {
558 self.lines.pos = self.lines.bytes.len();
559 break;
560 }
561 if !blank_opens_section(after) {
564 break;
565 }
566 self.lines.pos += next_eol.map(|p| p + 1).unwrap_or(rest.len());
568 if let Err(e) = self.advance_lines_to(coord_block_lines, end) {
569 return Some(Err(e));
570 }
571 }
572 Some(Ok(()))
573 }
574
575 pub fn forward(&mut self) -> Option<Result<(), error::ParseError>> {
587 self.forward_fast()
589 }
590
591 pub fn byte_pos(&self) -> usize {
593 self.lines.pos
594 }
595
596 pub fn skip_frames(&mut self, n: usize) -> Result<usize, error::ParseError> {
599 let mut skipped = 0usize;
600 for _ in 0..n {
601 match self.forward() {
602 None => break,
603 Some(Err(e)) => return Err(e),
604 Some(Ok(())) => skipped += 1,
605 }
606 }
607 Ok(skipped)
608 }
609
610 pub fn next_with_raw_span(
623 &mut self,
624 file_contents: &'a str,
625 ) -> Option<Result<(types::ConFrame, &'a str), error::ParseError>> {
626 let base = file_contents.as_ptr() as usize;
627 let start = {
628 let line = self.lines.peek_line()?;
629 line.as_ptr() as usize - base
630 };
631 let frame = match self.next()? {
632 Ok(f) => f,
633 Err(e) => return Some(Err(e)),
634 };
635 let end = match self.lines.peek_line() {
636 Some(line) => line.as_ptr() as usize - base,
637 None => file_contents.len(),
638 };
639 debug_assert!(end >= start && end <= file_contents.len());
640 Some(Ok((frame, &file_contents[start..end])))
641 }
642
643 pub(crate) fn next_with_end(
645 &mut self,
646 ) -> Option<Result<(types::ConFrame, usize), error::ParseError>> {
647 let frame = match self.next()? {
648 Ok(f) => f,
649 Err(e) => return Some(Err(e)),
650 };
651 let end = match self.lines.peek_line() {
652 Some(line) => line.as_ptr() as usize - self.lines.bytes.as_ptr() as usize,
653 None => self.lines.pos,
654 };
655 Some(Ok((frame, end)))
656 }
657}
658
659impl<'a> Iterator for ConFrameIterator<'a> {
660 type Item = Result<types::ConFrame, error::ParseError>;
665
666 fn next(&mut self) -> Option<Self::Item> {
673 if let Some(err) = self.lines.utf8_error() {
676 return Some(Err(err));
677 }
678 if self.lines.remainder_is_whitespace() {
682 if let Some(err) = self.lines.utf8_error() {
683 return Some(Err(err));
684 }
685 while self.lines.next_line().is_some() {}
686 return None;
687 }
688 if let Some(err) = self.lines.utf8_error() {
689 return Some(Err(err));
690 }
691 let mut frame = match crate::parser::parse_single_frame_ls(&mut self.lines) {
693 Ok(f) => f,
694 Err(e) => return Some(Err(self.lines.prefer_utf8(e))),
695 };
696 let sections =
700 match parse_declared_sections(&mut self.lines, &mut frame.header, &mut frame.atom_data)
701 {
702 Ok(n) => n,
703 Err(e) => return Some(Err(self.lines.prefer_utf8(e))),
706 };
707 if sections > 0 {
708 frame.sync_arrays_from_atom_data();
709 }
710 Some(Ok(frame))
711 }
712}
713
714#[cfg(test)]
715mod aos_soa_agreement_tests {
716 use super::*;
717 use std::path::PathBuf;
718
719 #[test]
720 fn iterator_vel_forces_soa_matches_aos() {
721 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
722 .join("resources/test/tiny_cuh2_vel_forces.con");
723 let text = std::fs::read_to_string(&p).expect("fixture");
724 let fr = ConFrameIterator::new(&text)
725 .next()
726 .expect("frame")
727 .expect("parse");
728 let n = fr.atom_data.len();
729 assert!(n > 0);
730 assert_eq!(fr.positions.nrows(), n);
731 let has_vel = fr.atom_data.iter().any(|a| a.velocity().is_some());
732 let has_frc = fr.atom_data.iter().any(|a| a.force().is_some());
733 if has_vel {
734 assert_eq!(
735 fr.velocities.nrows(),
736 n,
737 "SoA velocities must match AoS after section parse"
738 );
739 }
740 if has_frc {
741 assert_eq!(fr.forces.nrows(), n, "SoA forces must match AoS");
742 }
743 for (i, a) in fr.atom_data.iter().enumerate() {
744 let p = fr.positions.as_f64_row(i);
745 assert_eq!([a.x, a.y, a.z], p);
746 if let Some(v) = a.velocity() {
747 assert_eq!(v, fr.velocities.as_f64_row(i));
748 }
749 if let Some(f) = a.force() {
750 assert_eq!(f, fr.forces.as_f64_row(i));
751 }
752 }
753 }
754
755 #[test]
758 fn sync_skips_pos_when_nrows_matches_keeps_force_soa() {
759 let p =
760 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2_forces.con");
761 let text = std::fs::read_to_string(&p).expect("fixture");
762 let fr = ConFrameIterator::new(&text)
763 .next()
764 .expect("frame")
765 .expect("parse");
766 let n = fr.atom_data.len();
767 assert_eq!(fr.positions.nrows(), n);
768 assert_eq!(fr.forces.nrows(), n);
769 let p0 = fr.positions.as_f64_row(0);
772 let mut fr2 = fr.clone();
773 fr2.sync_arrays_from_atom_data();
774 assert_eq!(fr2.positions.as_f64_row(0), p0);
775 assert_eq!(fr2.forces.nrows(), n);
776 assert_eq!(
777 fr2.forces.as_f64_row(0),
778 fr2.atom_data[0].force().expect("force")
779 );
780 }
781}
782
783pub fn read_all_frames(path: &Path) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
789 read_all_frames_with_threads(path, None)
790}
791
792pub fn frames_from_text(
799 text: &str,
800 num_threads: Option<usize>,
801) -> Result<Vec<types::ConFrame>, error::ParseError> {
802 #[cfg(feature = "parallel")]
803 {
804 if let Some(n) = num_threads.filter(|n| *n >= 2) {
805 let parts = parse_frames_parallel_with_threads(text, Some(n));
806 let mut frames = Vec::with_capacity(parts.len());
807 for r in parts {
808 frames.push(r?);
809 }
810 return Ok(frames);
811 }
812 }
813 #[cfg(not(feature = "parallel"))]
814 {
815 let _ = num_threads;
816 }
817 ConFrameIterator::new(text).collect()
818}
819
820pub fn read_all_frames_with_threads(
827 path: &Path,
828 num_threads: Option<usize>,
829) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
830 let bytes = crate::compression::read_file_bytes(path)?;
831 Ok(frames_from_bytes(&bytes, num_threads)?)
832}
833
834pub struct PositionBlock {
842 pub n_frames: usize,
843 pub n_atoms: usize,
844 pub values: Vec<f64>,
845}
846
847pub fn read_positions_with_threads(
848 path: &Path,
849 num_threads: Option<usize>,
850) -> Result<PositionBlock, Box<dyn std::error::Error>> {
851 let contents = crate::compression::read_file_contents(path)?;
852 let block = match &contents {
853 crate::compression::FileContents::Owned(text) => {
854 fill_position_block(text.as_bytes(), num_threads)
855 }
856 crate::compression::FileContents::Mapped(map) => fill_position_block(map, num_threads),
857 }?;
858 Ok(block)
859}
860
861fn frame_starts(bytes: &[u8]) -> Result<Vec<usize>, error::ParseError> {
862 let mut scanner = ConFrameIterator::from_bytes(bytes);
863 let mut boundaries = Vec::new();
864 loop {
865 scanner.lines.clear_peek();
866 let start = scanner.lines.pos;
867 if start >= bytes.len() || bytes[start..].iter().all(u8::is_ascii_whitespace) {
868 break;
869 }
870 boundaries.push(start);
871 match scanner.forward_fast() {
872 Some(Ok(())) => {}
873 Some(Err(err)) => return Err(err),
874 None => break,
875 }
876 }
877 Ok(boundaries)
878}
879
880fn parse_span(
881 bytes: &[u8],
882 start: usize,
883 end: usize,
884) -> Result<types::ConFrame, error::ParseError> {
885 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
886 match iter.next() {
887 Some(result) => result,
888 None => Err(error::ParseError::IncompleteFrame),
889 }
890}
891
892fn copy_positions(frame: &types::ConFrame, dest: &mut [f64]) -> Result<(), error::ParseError> {
893 if frame.positions.nrows().saturating_mul(3) != dest.len() {
894 return Err(error::ParseError::InvalidCount(
895 "frame atom count does not match the first frame".into(),
896 ));
897 }
898 if let Some(src) = frame.positions.f64_slice() {
899 dest.copy_from_slice(src);
900 return Ok(());
901 }
902 for (i, slot) in dest.chunks_exact_mut(3).enumerate() {
903 slot.copy_from_slice(&frame.positions.as_f64_row(i));
904 }
905 Ok(())
906}
907
908fn fill_position_block(
909 bytes: &[u8],
910 num_threads: Option<usize>,
911) -> Result<PositionBlock, error::ParseError> {
912 let boundaries = frame_starts(bytes)?;
913 if boundaries.is_empty() {
914 return Ok(PositionBlock {
915 n_frames: 0,
916 n_atoms: 0,
917 values: Vec::new(),
918 });
919 }
920 let end0 = boundaries.get(1).copied().unwrap_or(bytes.len());
921 let first = parse_span(bytes, boundaries[0], end0)?;
922 let n_atoms = first.positions.nrows();
923 let n_frames = boundaries.len();
924 let stride = n_atoms.saturating_mul(3);
925 let mut values = vec![0.0_f64; n_frames.saturating_mul(stride)];
926 if stride == 0 {
927 return Ok(PositionBlock {
928 n_frames,
929 n_atoms,
930 values,
931 });
932 }
933 fill_position_spans(bytes, &boundaries, n_atoms, num_threads, &mut values)?;
934 Ok(PositionBlock {
935 n_frames,
936 n_atoms,
937 values,
938 })
939}
940
941fn fill_position_spans(
942 bytes: &[u8],
943 boundaries: &[usize],
944 n_atoms: usize,
945 num_threads: Option<usize>,
946 values: &mut [f64],
947) -> Result<(), error::ParseError> {
948 let stride = n_atoms * 3;
949 let write_one = |i: usize, dest: &mut [f64]| {
950 let start = boundaries[i];
951 let end = boundaries.get(i + 1).copied().unwrap_or(bytes.len());
952 let frame = parse_span(bytes, start, end)?;
953 copy_positions(&frame, dest)
954 };
955 #[cfg(feature = "parallel")]
956 {
957 let parallel = !matches!(num_threads, Some(n) if n <= 1);
958 if parallel {
959 use rayon::prelude::*;
960 let mut run = || {
961 values
962 .par_chunks_mut(stride)
963 .enumerate()
964 .try_for_each(|(i, dest)| write_one(i, dest))
965 };
966 return match num_threads {
967 Some(n) if n >= 2 => {
968 let pool = rayon::ThreadPoolBuilder::new()
969 .num_threads(n)
970 .build()
971 .map_err(|err| error::ParseError::ValidationError(err.to_string()))?;
972 pool.install(run)
973 }
974 _ => run(),
975 };
976 }
977 }
978 #[cfg(not(feature = "parallel"))]
979 {
980 let _ = num_threads;
981 }
982 for (i, dest) in values.chunks_mut(stride).enumerate() {
983 write_one(i, dest)?;
984 }
985 Ok(())
986}
987
988pub fn frames_from_bytes(
990 bytes: &[u8],
991 num_threads: Option<usize>,
992) -> Result<Vec<types::ConFrame>, error::ParseError> {
993 #[cfg(feature = "parallel")]
994 {
995 if let Some(n) = num_threads.filter(|n| *n >= 2) {
996 let parts = parse_frames_parallel_bytes(bytes, Some(n));
997 let mut frames = Vec::with_capacity(parts.len());
998 for r in parts {
999 frames.push(r?);
1000 }
1001 return Ok(frames);
1002 }
1003 }
1004 #[cfg(not(feature = "parallel"))]
1005 {
1006 let _ = num_threads;
1007 }
1008 let mut frames = Vec::new();
1009 for r in ConFrameIterator::from_bytes(bytes) {
1010 frames.push(r?);
1011 }
1012 Ok(frames)
1013}
1014
1015#[cfg(feature = "parallel")]
1016fn span_end(boundaries: &[usize], index: usize, len: usize) -> usize {
1017 if index + 1 < boundaries.len() {
1018 boundaries[index + 1]
1019 } else {
1020 len
1021 }
1022}
1023
1024#[cfg(feature = "parallel")]
1029#[derive(Copy, Clone)]
1030struct SlotWritePtr(*mut std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>);
1031
1032#[cfg(feature = "parallel")]
1033impl SlotWritePtr {
1034 unsafe fn write_slot(self, index: usize, value: Result<types::ConFrame, error::ParseError>) {
1037 unsafe {
1038 (*self.0.add(index)).write(value);
1039 }
1040 }
1041}
1042
1043#[cfg(feature = "parallel")]
1047unsafe impl Send for SlotWritePtr {}
1048
1049#[cfg(feature = "parallel")]
1052unsafe impl Sync for SlotWritePtr {}
1053
1054#[cfg(feature = "parallel")]
1056fn thread_minflt() -> i64 {
1057 unsafe {
1058 let mut usage = std::mem::zeroed::<libc::rusage>();
1059 libc::getrusage(libc::RUSAGE_THREAD, &mut usage);
1060 usage.ru_minflt
1061 }
1062}
1063
1064#[cfg(feature = "parallel")]
1070fn sample_llc_misses() -> i64 {
1071 #[repr(C)]
1072 struct PerfEventAttr {
1073 typ: u32,
1074 size: u32,
1075 config: u64,
1076 rest: [u64; 14],
1077 }
1078 let mut attr = PerfEventAttr {
1079 typ: 3,
1080 size: std::mem::size_of::<PerfEventAttr>() as u32,
1081 config: 2 | (1 << 16),
1083 rest: [0; 14],
1084 };
1085 let fd = unsafe {
1086 libc::syscall(
1087 libc::SYS_perf_event_open,
1088 &attr as *const PerfEventAttr,
1089 0i32,
1090 -1i32,
1091 -1i32,
1092 0u64,
1093 )
1094 };
1095 if fd < 0 {
1096 return -1;
1097 }
1098 let fd = fd as libc::c_int;
1099 let mut value = 0u64;
1100 let n = unsafe { libc::read(fd, &mut value as *mut u64 as *mut libc::c_void, 8) };
1101 unsafe {
1102 libc::close(fd);
1103 }
1104 if n == 8 { value as i64 } else { -1 }
1105}
1106
1107#[cfg(feature = "parallel")]
1113fn numa_remote_pages() -> i64 {
1114 let online = std::fs::read_to_string("/sys/devices/system/node/online").unwrap_or_default();
1115 let online = online.trim();
1116 let nodes = if let Some((start, end)) = online.split_once('-') {
1117 let start: i64 = start.parse().unwrap_or(0);
1118 let end: i64 = end.parse().unwrap_or(0);
1119 end - start + 1
1120 } else {
1121 1
1122 };
1123 if nodes <= 1 {
1124 return 0;
1125 }
1126 let text = std::fs::read_to_string("/proc/self/numa_maps").unwrap_or_default();
1127 let mut remote = 0i64;
1128 for line in text.lines() {
1129 for token in line.split_whitespace() {
1130 if let Some(rest) = token.strip_prefix('N') {
1131 if let Some((node, pages)) = rest.split_once('=') {
1132 if node != "0" {
1133 remote += pages.parse::<i64>().unwrap_or(0);
1134 }
1135 }
1136 }
1137 }
1138 }
1139 remote
1140}
1141
1142#[cfg(feature = "parallel")]
1149fn parse_into_slots<F>(
1150 bytes: &[u8],
1151 len: usize,
1152 boundaries: &[usize],
1153 workers: usize,
1154 parse_span: F,
1155) -> Vec<Result<types::ConFrame, error::ParseError>>
1156where
1157 F: Fn(usize, usize) -> Result<types::ConFrame, error::ParseError> + Sync,
1158{
1159 parse_into_slots_timed(bytes, len, boundaries, workers, parse_span).slots
1160}
1161
1162#[cfg(feature = "parallel")]
1164struct SlotParse {
1165 slots: Vec<Result<types::ConFrame, error::ParseError>>,
1166 alloc_s: f64,
1167 work_s: f64,
1168 sum_s: f64,
1169 max_s: f64,
1170 chunk_s: Vec<f64>,
1172 worker_minflt: Vec<i64>,
1174 share_frames: Vec<u64>,
1176 share_bytes: Vec<u64>,
1178}
1179
1180#[cfg(feature = "parallel")]
1188fn parse_into_slots_timed<F>(
1189 bytes: &[u8],
1190 len: usize,
1191 boundaries: &[usize],
1192 workers: usize,
1193 parse_span: F,
1194) -> SlotParse
1195where
1196 F: Fn(usize, usize) -> Result<types::ConFrame, error::ParseError> + Sync,
1197{
1198 let n = boundaries.len();
1199 if n == 0 {
1200 return SlotParse {
1201 slots: Vec::new(),
1202 alloc_s: 0.0,
1203 work_s: 0.0,
1204 sum_s: 0.0,
1205 max_s: 0.0,
1206 chunk_s: Vec::new(),
1207 worker_minflt: Vec::new(),
1208 share_frames: Vec::new(),
1209 share_bytes: Vec::new(),
1210 };
1211 }
1212 if workers < 2 {
1213 let t_alloc = std::time::Instant::now();
1214 let mut slots = Vec::with_capacity(n);
1215 let alloc_s = t_alloc.elapsed().as_secs_f64();
1216 let flt0 = thread_minflt();
1217 let t_work = std::time::Instant::now();
1218 for i in 0..n {
1219 slots.push(parse_span(boundaries[i], span_end(boundaries, i, len)));
1220 }
1221 let work_s = t_work.elapsed().as_secs_f64();
1222 let worker_minflt = vec![thread_minflt().saturating_sub(flt0)];
1223 return SlotParse {
1224 slots,
1225 alloc_s,
1226 work_s,
1227 sum_s: work_s,
1228 max_s: work_s,
1229 chunk_s: vec![work_s],
1230 worker_minflt,
1231 share_frames: vec![n as u64],
1232 share_bytes: vec![len as u64],
1233 };
1234 }
1235 let t_alloc = std::time::Instant::now();
1236 let mut slots = take_slot_buf(n);
1237 let alloc_s = t_alloc.elapsed().as_secs_f64();
1238 let share = n.div_ceil(workers);
1242 let chunk_ns: Vec<std::sync::atomic::AtomicU64> = (0..workers)
1243 .map(|_| std::sync::atomic::AtomicU64::new(0))
1244 .collect();
1245 let chunk_flt: Vec<std::sync::atomic::AtomicI64> = (0..workers)
1246 .map(|_| std::sync::atomic::AtomicI64::new(0))
1247 .collect();
1248 let slots_ptr = SlotWritePtr(slots.as_mut_ptr());
1249 let t_work = std::time::Instant::now();
1250 {
1251 let parse_share = |worker: usize| {
1252 let start = worker * share;
1253 if start >= n {
1254 return;
1255 }
1256 let end = (start + share).min(n);
1257 let flt0 = thread_minflt();
1258 let t_chunk = std::time::Instant::now();
1259 let byte_start = boundaries[start];
1263 let byte_end = span_end(boundaries, end - 1, len);
1264 let mut iter = ConFrameIterator::from_bytes(&bytes[byte_start..byte_end]);
1265 for i in start..end {
1266 let frame = match iter.next() {
1267 Some(result) => result,
1268 None => Err(error::ParseError::IncompleteFrame),
1269 };
1270 unsafe {
1273 slots_ptr.write_slot(i, frame);
1274 }
1275 }
1276 chunk_ns[worker].store(
1277 t_chunk.elapsed().as_nanos() as u64,
1278 std::sync::atomic::Ordering::Relaxed,
1279 );
1280 chunk_flt[worker].store(
1281 thread_minflt().saturating_sub(flt0),
1282 std::sync::atomic::Ordering::Relaxed,
1283 );
1284 };
1285 if rayon::current_thread_index().is_some() {
1289 rayon::broadcast(|ctx| {
1290 let worker = ctx.index();
1291 if worker < workers {
1292 parse_share(worker);
1293 }
1294 });
1295 } else {
1296 use rayon::prelude::*;
1297 (0..workers).into_par_iter().for_each(parse_share);
1298 }
1299 }
1300 let work_s = t_work.elapsed().as_secs_f64();
1301 let mut worker_sum_s = 0.0_f64;
1302 let mut worker_max_s = 0.0_f64;
1303 let mut chunk_s = Vec::new();
1304 let mut worker_minflt = Vec::new();
1305 let mut share_frames = Vec::new();
1306 let mut share_bytes = Vec::new();
1307 for worker in 0..workers {
1308 let start = worker * share;
1309 if start >= n {
1310 continue;
1311 }
1312 let seconds = chunk_ns[worker].load(std::sync::atomic::Ordering::Relaxed) as f64 / 1.0e9;
1313 chunk_s.push(seconds);
1314 worker_sum_s += seconds;
1315 worker_max_s = worker_max_s.max(seconds);
1316 worker_minflt.push(chunk_flt[worker].load(std::sync::atomic::Ordering::Relaxed));
1317 let end = (start + share).min(n);
1318 share_frames.push((end - start) as u64);
1319 let bytes = span_end(boundaries, end - 1, len).saturating_sub(boundaries[start]);
1320 share_bytes.push(bytes as u64);
1321 }
1322 let mut owned = Vec::with_capacity(n);
1326 for index in 0..n {
1327 owned.push(unsafe { slots.as_mut_ptr().add(index).read().assume_init() });
1328 }
1329 unsafe { slots.set_len(0); }
1330 recycle_slot_buf(slots);
1331 let slots = owned;
1332 SlotParse {
1333 slots,
1334 alloc_s,
1335 work_s,
1336 sum_s: worker_sum_s,
1337 max_s: worker_max_s,
1338 chunk_s,
1339 worker_minflt,
1340 share_frames,
1341 share_bytes,
1342 }
1343}
1344
1345#[cfg(feature = "parallel")]
1346fn parse_byte_span(
1347 bytes: &[u8],
1348 start: usize,
1349 end: usize,
1350) -> Result<types::ConFrame, error::ParseError> {
1351 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
1352 match iter.next() {
1353 Some(result) => result,
1354 None => Err(error::ParseError::IncompleteFrame),
1355 }
1356}
1357
1358#[cfg(feature = "parallel")]
1359fn parse_text_span(
1360 text: &str,
1361 start: usize,
1362 end: usize,
1363) -> Result<types::ConFrame, error::ParseError> {
1364 let mut iter = ConFrameIterator::new(&text[start..end]);
1365 match iter.next() {
1366 Some(result) => result,
1367 None => Err(error::ParseError::IncompleteFrame),
1368 }
1369}
1370
1371#[cfg(feature = "parallel")]
1372struct TimedParse {
1373 slots: Vec<Result<types::ConFrame, error::ParseError>>,
1374 pool: Option<std::sync::Arc<rayon::ThreadPool>>,
1377 pool_s: f64,
1378 pool_join_s: f64,
1380 scan_s: f64,
1381 scan_minflt: i64,
1382 slot_alloc_s: f64,
1383 worker_parse_s: f64,
1384 worker_sum_s: f64,
1385 worker_max_s: f64,
1386 chunk_s: Vec<f64>,
1387 worker_minflt: Vec<i64>,
1388 share_frames: Vec<u64>,
1389 share_bytes: Vec<u64>,
1390 llc_misses: i64,
1391 numa_remote: i64,
1392 scan_kind: u8,
1394}
1395
1396#[cfg(feature = "parallel")]
1402fn keep_parse_pages() {
1403 unsafe {
1404 libc::mallopt(-4, 0);
1406 libc::mallopt(-1, i32::MAX);
1407 }
1408}
1409
1410#[cfg(feature = "parallel")]
1416fn take_slot_buf(
1417 n: usize,
1418) -> Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>> {
1419 let mut buf = slot_cache()
1420 .lock()
1421 .map(|mut slot| std::mem::take(&mut *slot))
1422 .unwrap_or_default();
1423 if buf.capacity() < n {
1424 buf = Vec::with_capacity(n);
1425 }
1426 unsafe { buf.set_len(n) };
1427 buf
1428}
1429
1430#[cfg(feature = "parallel")]
1431fn recycle_slot_buf(
1432 mut buf: Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>>,
1433) {
1434 unsafe { buf.set_len(0) };
1435 if let Ok(mut slot) = slot_cache().lock() {
1436 if buf.capacity() >= slot.capacity() {
1437 *slot = buf;
1438 }
1439 }
1440}
1441
1442#[cfg(feature = "parallel")]
1443fn slot_cache() -> &'static std::sync::Mutex<
1444 Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>>,
1445> {
1446 static CACHE: std::sync::Mutex<
1447 Vec<std::mem::MaybeUninit<Result<types::ConFrame, error::ParseError>>>,
1448 > = std::sync::Mutex::new(Vec::new());
1449 &CACHE
1450}
1451
1452#[cfg(feature = "parallel")]
1457fn shared_pool(workers: usize) -> Option<std::sync::Arc<rayon::ThreadPool>> {
1458 use std::sync::{Arc, Mutex, OnceLock};
1459 static CACHE: OnceLock<Mutex<Vec<(usize, Arc<rayon::ThreadPool>)>>> = OnceLock::new();
1460 let cache = CACHE.get_or_init(|| Mutex::new(Vec::new()));
1461 let mut guard = cache.lock().ok()?;
1462 if let Some((_, pool)) = guard.iter().find(|(count, _)| *count == workers) {
1463 return Some(Arc::clone(pool));
1464 }
1465 keep_parse_pages();
1466 let pool = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(workers).build().ok()?);
1467 guard.push((workers, Arc::clone(&pool)));
1468 Some(pool)
1469}
1470
1471#[cfg(feature = "parallel")]
1473pub fn process_minflt() -> i64 {
1474 unsafe {
1475 let mut usage = std::mem::zeroed::<libc::rusage>();
1476 libc::getrusage(libc::RUSAGE_SELF, &mut usage);
1477 usage.ru_minflt
1478 }
1479}
1480
1481#[cfg(feature = "parallel")]
1483fn host_page_size() -> usize {
1484 let raw = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
1485 if raw > 0 { raw as usize } else { 4096 }
1486}
1487
1488#[cfg(feature = "parallel")]
1493fn first_touch_f64(values: &mut [f64], workers: usize) {
1494 use rayon::prelude::*;
1495 if values.is_empty() || workers < 2 {
1496 return;
1497 }
1498 let page = host_page_size().max(std::mem::size_of::<f64>());
1499 let step = (page / std::mem::size_of::<f64>()).max(1);
1500 let pages = values.len().div_ceil(step);
1501 let share = pages.div_ceil(workers);
1502 if rayon::current_thread_index().is_some() {
1503 values.par_chunks_mut(step).for_each(|chunk| {
1506 if let Some(slot) = chunk.first_mut() {
1507 *slot = 0.0;
1508 }
1509 });
1510 return;
1511 }
1512 std::thread::scope(|scope| {
1513 let mut rest = values;
1514 for worker in 0..workers {
1515 if rest.is_empty() {
1516 break;
1517 }
1518 let pages_here = share.min(rest.len().div_ceil(step));
1519 let take = (pages_here * step).min(rest.len());
1520 let (head, tail) = rest.split_at_mut(take);
1521 rest = tail;
1522 let _ = worker;
1523 scope.spawn(move || {
1524 let mut at = 0;
1525 while at < head.len() {
1526 head[at] = 0.0;
1527 at += step;
1528 }
1529 });
1530 }
1531 });
1532}
1533
1534#[cfg(feature = "parallel")]
1540fn finish_results(
1541 pool: Option<&rayon::ThreadPool>,
1542 slots: Vec<Result<types::ConFrame, error::ParseError>>,
1543) -> Result<Vec<types::ConFrame>, error::ParseError> {
1544 if slots.len() < 2 || pool.is_none() {
1545 let mut frames = Vec::with_capacity(slots.len());
1546 for part in slots {
1547 frames.push(part?);
1548 }
1549 return Ok(frames);
1550 }
1551 let pool = pool.expect("pool");
1552 pool.install(|| {
1553 use rayon::prelude::*;
1554 if let Some(index) = slots.par_iter().position_first(|item| item.is_err()) {
1555 return Err(match slots.into_iter().nth(index) {
1556 Some(Err(err)) => err,
1557 _ => error::ParseError::IncompleteFrame,
1558 });
1559 }
1560 Ok(slots.into_par_iter().map(|item| item.expect("ok")).collect())
1561 })
1562}
1563
1564#[cfg(feature = "parallel")]
1566pub const SCAN_SERIAL: u8 = 0;
1567#[cfg(feature = "parallel")]
1574pub const SCAN_INDEXED: u8 = 1;
1575#[cfg(feature = "parallel")]
1577pub const SCAN_EQUAL_SPAN: u8 = 2;
1578
1579#[cfg(feature = "parallel")]
1587fn equal_span_boundaries(bytes: &[u8], workers: usize) -> Option<Vec<usize>> {
1588 if workers < 2 || bytes.len() < 2 {
1589 return None;
1590 }
1591 let mut first = ConFrameIterator::from_bytes(bytes);
1592 match first.forward_fast() {
1593 Some(Ok(())) => {}
1594 _ => return None,
1595 }
1596 let span = first.lines.pos;
1597 if span < 2 || bytes.len() % span != 0 {
1598 return None;
1599 }
1600 let n = bytes.len() / span;
1601 if n < 2 {
1602 return None;
1603 }
1604 let byte_share = bytes.len().div_ceil(workers);
1608 for worker in 0..workers {
1609 let byte_cut = worker.saturating_mul(byte_share).min(bytes.len());
1610 if byte_cut >= bytes.len() {
1611 continue;
1612 }
1613 let index = if worker == 0 {
1614 0
1615 } else {
1616 byte_cut.div_ceil(span).min(n - 1)
1617 };
1618 let start = index.saturating_mul(span);
1619 if start >= bytes.len() {
1620 continue;
1621 }
1622 let end = (start + span).min(bytes.len());
1623 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
1624 match iter.forward_fast() {
1625 Some(Ok(())) if iter.lines.pos == end - start => {}
1626 _ => return None,
1627 }
1628 }
1629 Some(arithmetic_starts(span, n))
1630}
1631
1632#[cfg(feature = "parallel")]
1637static START_CACHE: std::sync::Mutex<Vec<usize>> = std::sync::Mutex::new(Vec::new());
1638
1639#[cfg(feature = "parallel")]
1640fn arithmetic_starts(span: usize, n: usize) -> Vec<usize> {
1641 let mut starts = START_CACHE
1642 .lock()
1643 .map(|mut slot| std::mem::take(&mut *slot))
1644 .unwrap_or_default();
1645 if starts.len() != n {
1646 starts.resize(n, 0);
1647 }
1648 for (index, slot) in starts.iter_mut().enumerate() {
1649 *slot = index * span;
1650 }
1651 starts
1652}
1653
1654#[cfg(feature = "parallel")]
1655fn recycle_starts(starts: Vec<usize>) {
1656 if let Ok(mut slot) = START_CACHE.lock() {
1657 if starts.capacity() >= slot.capacity() {
1658 *slot = starts;
1659 }
1660 }
1661}
1662
1663#[cfg(feature = "parallel")]
1664fn parse_frames_parallel_bytes(
1665 bytes: &[u8],
1666 num_threads: Option<usize>,
1667) -> Vec<Result<types::ConFrame, error::ParseError>> {
1668 parse_bytes_timed(bytes, num_threads).slots
1669}
1670
1671#[cfg(feature = "parallel")]
1678fn parse_bytes_timed(bytes: &[u8], num_threads: Option<usize>) -> TimedParse {
1679 let threads = scan_thread_count(num_threads, bytes.len());
1680 let parse_workers = match num_threads {
1684 Some(n) if n >= 2 => n,
1685 Some(_) => 1,
1686 None => threads,
1687 };
1688 let run = || {
1689 let fault0 = process_minflt();
1690 let t_scan = std::time::Instant::now();
1691 let (boundaries, scan_kind) =
1692 if let Some(bounds) = equal_span_boundaries(bytes, parse_workers) {
1693 (bounds, SCAN_EQUAL_SPAN)
1694 } else {
1695 let mut scanner = ConFrameIterator::from_bytes(bytes);
1696 let scan_kind = if threads >= 2 {
1697 scanner.lines.newlines = Some(newline_positions(bytes, threads));
1701 scanner.lines.line_no = 0;
1702 SCAN_INDEXED
1703 } else {
1704 SCAN_SERIAL
1705 };
1706 (collect_frame_starts(scanner, true), scan_kind)
1708 };
1709 let scan_s = t_scan.elapsed().as_secs_f64();
1710 let scan_minflt = process_minflt().saturating_sub(fault0);
1711 let parsed =
1712 parse_into_slots_timed(bytes, bytes.len(), &boundaries, parse_workers, |start, end| {
1713 parse_byte_span(bytes, start, end)
1714 });
1715 recycle_starts(boundaries);
1716 (parsed, scan_s, scan_kind, scan_minflt)
1717 };
1718 let (pool_s, pool, packed) = match num_threads {
1719 None => (0.0, None, run()),
1720 Some(n) if n <= 1 => {
1721 let _ = n;
1722 keep_parse_pages();
1723 let fault0 = process_minflt();
1724 let t_scan = std::time::Instant::now();
1725 let boundaries = collect_frame_starts(ConFrameIterator::from_bytes(bytes), true);
1726 let scan_s = t_scan.elapsed().as_secs_f64();
1727 let scan_minflt = process_minflt().saturating_sub(fault0);
1728 let parsed = parse_into_slots_timed(bytes, bytes.len(), &boundaries, 1, |start, end| {
1729 parse_byte_span(bytes, start, end)
1730 });
1731 (0.0, None, (parsed, scan_s, SCAN_SERIAL, scan_minflt))
1732 }
1733 Some(n) => {
1734 let t_pool = std::time::Instant::now();
1735 match shared_pool(n) {
1736 Some(pool) => {
1737 let built = t_pool.elapsed().as_secs_f64();
1738 let t_wake = std::time::Instant::now();
1739 let (wake, body) = pool.install(|| {
1740 let wake = t_wake.elapsed().as_secs_f64();
1741 (wake, run())
1742 });
1743 (built + wake, Some(pool), body)
1746 }
1747 None => (t_pool.elapsed().as_secs_f64(), None, run()),
1748 }
1749 }
1750 };
1751 let (parsed, scan_s, scan_kind, scan_minflt) = packed;
1752 TimedParse {
1753 slots: parsed.slots,
1754 pool,
1755 pool_s,
1756 pool_join_s: 0.0,
1757 scan_s,
1758 scan_minflt,
1759 slot_alloc_s: parsed.alloc_s,
1760 worker_parse_s: parsed.work_s,
1761 worker_sum_s: parsed.sum_s,
1762 worker_max_s: parsed.max_s,
1763 chunk_s: parsed.chunk_s,
1764 worker_minflt: parsed.worker_minflt,
1765 share_frames: parsed.share_frames,
1766 share_bytes: parsed.share_bytes,
1767 llc_misses: sample_llc_misses(),
1768 numa_remote: numa_remote_pages(),
1769 scan_kind,
1770 }
1771}
1772
1773pub fn count_frames(path: &Path) -> Result<usize, Box<dyn std::error::Error>> {
1778 let contents = crate::compression::read_file_contents(path)?;
1779 let text = contents.as_str()?;
1780 let mut n = 0usize;
1781 let mut iter = ConFrameIterator::new(text);
1782 loop {
1783 match iter.forward_fast() {
1784 Some(Ok(())) => n += 1,
1785 Some(Err(e)) => return Err(Box::new(e)),
1786 None => break,
1787 }
1788 }
1789 Ok(n)
1790}
1791
1792pub fn read_first_frame(path: &Path) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
1797 let contents = crate::compression::read_file_contents(path)?;
1798 let text = contents.as_str()?;
1799 let mut iter = ConFrameIterator::new(text);
1800 match iter.next() {
1801 Some(Ok(frame)) => Ok(frame),
1802 Some(Err(e)) => Err(Box::new(e)),
1803 None => Err("No frames found in file".into()),
1804 }
1805}
1806
1807pub fn read_nth_frame(
1811 path: &Path,
1812 index: usize,
1813) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
1814 let contents = crate::compression::read_file_contents(path)?;
1815 let text = contents.as_str()?;
1816 read_nth_frame_from_text(text, index)
1817}
1818
1819pub fn read_nth_frame_from_text(
1821 text: &str,
1822 index: usize,
1823) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
1824 let mut iter = ConFrameIterator::new(text);
1825 let skipped = iter.skip_frames(index)?;
1826 if skipped < index {
1827 return Err(format!("frame {index} out of range (only {skipped} frames)").into());
1828 }
1829 match iter.next() {
1830 Some(Ok(frame)) => Ok(frame),
1831 Some(Err(e)) => Err(Box::new(e)),
1832 None => Err(format!("frame {index} out of range").into()),
1833 }
1834}
1835
1836#[cfg(feature = "parallel")]
1842pub fn read_all_frames_indexed(
1843 path: &Path,
1844 num_threads: Option<usize>,
1845) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
1846 let contents = crate::compression::read_file_contents(path)?;
1847 let text = contents.as_str_sliced()?;
1848 let idx = match contents {
1849 crate::compression::FileContents::Mapped(_) => {
1850 crate::frame_index::load_or_build(path, text, true)?
1851 }
1852 crate::compression::FileContents::Owned(_) => {
1853 let on_disk = std::fs::metadata(path)?.len();
1854 crate::frame_index::load_or_build(path, text, on_disk == text.len() as u64)?
1855 }
1856 };
1857 let offs: Vec<usize> = idx.offsets.iter().map(|&o| o as usize).collect();
1858 parse_frames_at_offsets(text, &offs, num_threads)
1859 .into_iter()
1860 .map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error>))
1861 .collect()
1862}
1863
1864enum IndexStep {
1865 Stay(usize),
1866 Missing(usize),
1867 Landed(usize, usize),
1868 Short(usize, usize),
1869}
1870
1871struct IndexedSpan {
1872 start: usize,
1873 content_end: usize,
1874 next_pos: usize,
1875}
1876
1877fn line_count(nl: &[usize], len: usize) -> usize {
1879 if len == 0 {
1880 0
1881 } else if nl.last().copied() == Some(len - 1) {
1882 nl.len()
1883 } else {
1884 nl.len() + 1
1885 }
1886}
1887
1888fn line_start_pos(nl: &[usize], len: usize, line_no: usize) -> usize {
1889 if line_no == 0 {
1890 return 0;
1891 }
1892 if line_no >= line_count(nl, len) {
1893 return len;
1894 }
1895 nl[line_no - 1] + 1
1896}
1897
1898fn line_no_for_pos(nl: &[usize], len: usize, pos: usize) -> usize {
1900 if pos >= len {
1901 return line_count(nl, len);
1902 }
1903 nl.partition_point(|&p| p < pos)
1904}
1905
1906fn indexed_span(nl: &[usize], bytes: &[u8], line_no: usize) -> Option<IndexedSpan> {
1907 let len = bytes.len();
1908 if line_no >= line_count(nl, len) {
1909 return None;
1910 }
1911 let start = if line_no == 0 { 0 } else { nl[line_no - 1] + 1 };
1912 let (raw_end, next_pos) = if line_no < nl.len() {
1913 (nl[line_no], nl[line_no] + 1)
1914 } else {
1915 (len, len)
1916 };
1917 let mut content_end = raw_end;
1918 if content_end > start && bytes[content_end - 1] == b'\r' {
1919 content_end -= 1;
1920 }
1921 Some(IndexedSpan {
1922 start,
1923 content_end,
1924 next_pos,
1925 })
1926}
1927
1928#[cfg(feature = "parallel")]
1930fn equal_cuts(len: usize, n: usize) -> Vec<usize> {
1931 let n = n.max(1);
1932 let mut cuts = Vec::with_capacity(n + 1);
1933 for i in 0..=n {
1934 let at = ((i as u128) * (len as u128) / (n as u128)) as usize;
1935 let at = at.min(len);
1936 if cuts.last().copied() != Some(at) {
1937 cuts.push(at);
1938 }
1939 }
1940 if cuts.last().copied() != Some(len) {
1941 cuts.push(len);
1942 }
1943 cuts
1944}
1945
1946#[cfg(feature = "parallel")]
1953fn newline_positions_at_cuts(bytes: &[u8], cuts: &[usize]) -> Vec<usize> {
1954 let spans: Vec<(usize, usize)> = cuts
1955 .windows(2)
1956 .map(|w| (w[0].min(bytes.len()), w[1].min(bytes.len())))
1957 .filter(|(start, end)| start < end)
1958 .collect();
1959 if spans.len() <= 1 {
1960 let (start, end) = spans.first().copied().unwrap_or((0, 0));
1961 return memchr::memchr_iter(b'\n', &bytes[start..end])
1962 .map(|p| start + p)
1963 .collect();
1964 }
1965 use rayon::prelude::*;
1969 let parts: Vec<Vec<usize>> = spans
1970 .par_iter()
1971 .map(|&(start, end)| {
1972 memchr::memchr_iter(b'\n', &bytes[start..end])
1973 .map(|p| start + p)
1974 .collect()
1975 })
1976 .collect();
1977 let mut all = Vec::with_capacity(bytes.len() / 32);
1978 for part in parts {
1979 all.extend(part);
1980 }
1981 all
1982}
1983
1984#[cfg(feature = "parallel")]
1985fn newline_positions(bytes: &[u8], threads: usize) -> Vec<usize> {
1986 newline_positions_at_cuts(bytes, &equal_cuts(bytes.len(), threads))
1987}
1988
1989#[cfg(feature = "parallel")]
1999fn scan_thread_count(num_threads: Option<usize>, len: usize) -> usize {
2000 let requested = match num_threads {
2001 Some(n) if n >= 2 => n,
2002 _ => return 1,
2003 };
2004 if len < 2 { 1 } else { requested }
2005}
2006
2007fn collect_frame_starts(
2008 mut scanner: ConFrameIterator<'_>,
2009 skip_whitespace_tail: bool,
2010) -> Vec<usize> {
2011 let mut boundaries = Vec::new();
2012 loop {
2013 scanner.lines.clear_peek();
2014 let start = scanner.lines.pos;
2015 if start >= scanner.lines.bytes.len() {
2016 break;
2017 }
2018 if skip_whitespace_tail {
2019 let rest = &scanner.lines.bytes[start..];
2020 if rest.iter().all(u8::is_ascii_whitespace) {
2021 break;
2022 }
2023 }
2024 boundaries.push(start);
2025 match scanner.forward_fast() {
2026 Some(Ok(())) => {}
2027 Some(Err(_)) | None => break,
2028 }
2029 }
2030 boundaries
2031}
2032
2033pub fn frame_start_offsets_serial(file_contents: &str) -> Vec<usize> {
2035 collect_frame_starts(ConFrameIterator::new(file_contents), false)
2036}
2037
2038#[cfg(feature = "parallel")]
2039fn scanner_with_newlines<'a>(text: &'a str, newlines: Vec<usize>) -> ConFrameIterator<'a> {
2040 let mut scanner = ConFrameIterator::new(text);
2041 scanner.lines.line_no = 0;
2042 scanner.lines.newlines = Some(newlines);
2043 scanner
2044}
2045
2046#[cfg(feature = "parallel")]
2047fn frame_start_offsets_indexed(text: &str, newlines: Vec<usize>) -> Vec<usize> {
2048 collect_frame_starts(scanner_with_newlines(text, newlines), false)
2049}
2050
2051#[cfg(all(feature = "parallel", test))]
2057fn frame_start_offsets_at_cuts(text: &str, cuts: &[usize]) -> Vec<usize> {
2058 let newlines = newline_positions_at_cuts(text.as_bytes(), cuts);
2059 frame_start_offsets_indexed(text, newlines)
2060}
2061
2062pub fn frame_start_offsets(file_contents: &str) -> Vec<usize> {
2074 frame_start_offsets_with_threads(file_contents, None)
2075}
2076
2077pub fn frame_start_offsets_with_threads(
2085 file_contents: &str,
2086 num_threads: Option<usize>,
2087) -> Vec<usize> {
2088 #[cfg(feature = "parallel")]
2089 {
2090 let threads = scan_thread_count(num_threads, file_contents.len());
2091 if threads >= 2 {
2092 let newlines = newline_positions(file_contents.as_bytes(), threads);
2093 return frame_start_offsets_indexed(file_contents, newlines);
2094 }
2095 }
2096 #[cfg(not(feature = "parallel"))]
2097 {
2098 let _ = num_threads;
2099 }
2100 frame_start_offsets_serial(file_contents)
2101}
2102
2103#[cfg(feature = "parallel")]
2118pub fn parse_frames_parallel(
2119 file_contents: &str,
2120) -> Vec<Result<types::ConFrame, error::ParseError>> {
2121 parse_frames_parallel_with_threads(file_contents, None)
2122}
2123
2124#[cfg(feature = "parallel")]
2136pub fn parse_frames_parallel_with_threads(
2137 file_contents: &str,
2138 num_threads: Option<usize>,
2139) -> Vec<Result<types::ConFrame, error::ParseError>> {
2140 let threads = scan_thread_count(num_threads, file_contents.len());
2144 let parse_workers = match num_threads {
2145 Some(n) if n >= 2 => n,
2146 Some(_) => 1,
2147 None => threads,
2148 };
2149 let run = || {
2150 let boundaries = if threads >= 2 {
2154 let newlines = newline_positions(file_contents.as_bytes(), threads);
2155 collect_frame_starts(scanner_with_newlines(file_contents, newlines), true)
2156 } else {
2157 collect_frame_starts(ConFrameIterator::new(file_contents), true)
2158 };
2159 parse_into_slots(
2160 file_contents.as_bytes(),
2161 file_contents.len(),
2162 &boundaries,
2163 parse_workers,
2164 |start, end| parse_text_span(file_contents, start, end),
2165 )
2166 };
2167 match num_threads {
2168 Some(n) if n >= 2 => match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
2169 Ok(pool) => pool.install(run),
2170 Err(_) => run(),
2171 },
2172 _ => run(),
2173 }
2174}
2175
2176#[cfg(feature = "parallel")]
2191pub fn parse_frames_at_offsets(
2192 file_contents: &str,
2193 boundaries: &[usize],
2194 num_threads: Option<usize>,
2195) -> Vec<Result<types::ConFrame, error::ParseError>> {
2196 let workers = match num_threads {
2197 Some(n) if n <= 1 => 1,
2198 Some(n) => n,
2199 None => rayon::current_num_threads().max(1),
2200 };
2201 let run = || {
2202 parse_into_slots(file_contents.as_bytes(), file_contents.len(), boundaries, workers, |start, end| {
2203 parse_text_span(file_contents, start, end)
2204 })
2205 };
2206 match num_threads {
2207 Some(n) if n >= 2 => match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
2208 Ok(pool) => pool.install(run),
2209 Err(_) => run(),
2210 },
2211 _ => run(),
2212 }
2213}
2214
2215#[derive(Clone, Copy, Debug, Default)]
2218pub struct ReadPhaseTimes {
2219 pub open_mmap_s: f64,
2220 pub pool_s: f64,
2222 pub pool_join_s: f64,
2225 pub scan_s: f64,
2226 pub slot_alloc_s: f64,
2227 pub worker_parse_s: f64,
2228 pub finish_s: f64,
2230 pub worker_sum_s: f64,
2232 pub worker_max_s: f64,
2234 pub close_s: f64,
2236 pub schedule_s: f64,
2238 pub scan_minflt: i64,
2240 pub parse_minflt: i64,
2242 pub llc_misses: i64,
2244 pub numa_remote: i64,
2246 pub slab_minflt: i64,
2248 pub touch_minflt: i64,
2250 pub mapped: bool,
2252}
2253
2254pub struct PhasedFrames {
2256 pub frames: Vec<types::ConFrame>,
2257 pub phases: ReadPhaseTimes,
2258 pub n_bytes: usize,
2259 pub chunk_s: Vec<f64>,
2261 pub scan_kind: u8,
2264 pub worker_minflt: Vec<i64>,
2266 pub share_frames: Vec<u64>,
2268 pub share_bytes: Vec<u64>,
2270 #[cfg(feature = "parallel")]
2272 pub pool: Option<std::sync::Arc<rayon::ThreadPool>>,
2273}
2274
2275pub fn read_all_frames_phased(
2282 path: &Path,
2283 num_threads: Option<usize>,
2284) -> Result<PhasedFrames, Box<dyn std::error::Error>> {
2285 let t_all = std::time::Instant::now();
2286 let t_open = std::time::Instant::now();
2287 let contents = crate::compression::read_file_contents(path)?;
2288 let open_mmap_s = t_open.elapsed().as_secs_f64();
2289 let mapped = matches!(contents, crate::compression::FileContents::Mapped(_));
2290 let n_bytes = match &contents {
2291 crate::compression::FileContents::Owned(text) => text.len(),
2292 crate::compression::FileContents::Mapped(map) => map.len(),
2293 };
2294 let bytes: &[u8] = match &contents {
2295 crate::compression::FileContents::Owned(text) => text.as_bytes(),
2296 crate::compression::FileContents::Mapped(map) => map,
2297 };
2298 let phased = frames_from_bytes_phased(bytes, num_threads)?;
2299 let workers = match num_threads {
2300 Some(n) if n >= 2 => n,
2301 _ => 1,
2302 };
2303 let t_close = std::time::Instant::now();
2304 if let crate::compression::FileContents::Mapped(map) = &contents {
2305 if workers >= 2 {
2310 #[cfg(feature = "parallel")]
2311 {
2312 if let Some(pool) = phased.pool.as_ref() {
2313 pool.install(|| {
2314 crate::compression::advise_dontneed_parallel(map, workers);
2315 });
2316 }
2317 }
2318 #[cfg(not(feature = "parallel"))]
2319 {
2320 let _ = (map, workers);
2321 }
2322 }
2323 }
2324 drop(contents);
2325 let close_s = t_close.elapsed().as_secs_f64();
2326 let elapsed = t_all.elapsed().as_secs_f64();
2327 let named = open_mmap_s
2328 + phased.pool_s
2329 + phased.pool_join_s
2330 + phased.scan_s
2331 + phased.slot_alloc_s
2332 + phased.worker_parse_s
2333 + phased.finish_s
2334 + close_s;
2335 let schedule_s = (elapsed - named).max(0.0);
2336 let parse_minflt = phased.worker_minflt.iter().sum();
2337 Ok(PhasedFrames {
2338 frames: phased.frames,
2339 n_bytes,
2340 chunk_s: phased.chunk_s,
2341 scan_kind: phased.scan_kind,
2342 worker_minflt: phased.worker_minflt,
2343 share_frames: phased.share_frames,
2344 share_bytes: phased.share_bytes,
2345 phases: ReadPhaseTimes {
2346 open_mmap_s,
2347 pool_s: phased.pool_s,
2348 pool_join_s: phased.pool_join_s,
2349 scan_s: phased.scan_s,
2350 slot_alloc_s: phased.slot_alloc_s,
2351 worker_parse_s: phased.worker_parse_s,
2352 finish_s: phased.finish_s,
2353 worker_sum_s: phased.worker_sum_s,
2354 worker_max_s: phased.worker_max_s,
2355 close_s,
2356 schedule_s,
2357 scan_minflt: phased.scan_minflt,
2358 parse_minflt,
2359 llc_misses: phased.llc_misses,
2360 numa_remote: phased.numa_remote,
2361 mapped,
2362 ..ReadPhaseTimes::default()
2363 },
2364 #[cfg(feature = "parallel")]
2365 pool: phased.pool,
2366 })
2367}
2368
2369pub struct PhasedByteTimes {
2375 pub frames: Vec<types::ConFrame>,
2376 pub pool_s: f64,
2377 pub pool_join_s: f64,
2378 pub scan_s: f64,
2379 pub slot_alloc_s: f64,
2380 pub worker_parse_s: f64,
2381 pub finish_s: f64,
2382 pub worker_sum_s: f64,
2383 pub worker_max_s: f64,
2384 pub chunk_s: Vec<f64>,
2386 pub scan_kind: u8,
2389 pub scan_minflt: i64,
2391 pub worker_minflt: Vec<i64>,
2393 pub share_frames: Vec<u64>,
2394 pub share_bytes: Vec<u64>,
2395 pub llc_misses: i64,
2397 pub numa_remote: i64,
2399 #[cfg(feature = "parallel")]
2401 pub pool: Option<std::sync::Arc<rayon::ThreadPool>>,
2402}
2403
2404pub fn frames_from_bytes_phased(
2405 bytes: &[u8],
2406 num_threads: Option<usize>,
2407) -> Result<PhasedByteTimes, error::ParseError> {
2408 #[cfg(feature = "parallel")]
2409 {
2410 if !matches!(num_threads, Some(n) if n >= 2) {
2413 let flt0 = thread_minflt();
2414 let t_work = std::time::Instant::now();
2415 let mut frames = Vec::new();
2416 for part in ConFrameIterator::from_bytes(bytes) {
2417 frames.push(part?);
2418 }
2419 let worker_parse_s = t_work.elapsed().as_secs_f64();
2420 let worker_flt = thread_minflt().saturating_sub(flt0);
2421 let n_frames = frames.len() as u64;
2422 return Ok(PhasedByteTimes {
2423 frames,
2424 pool_s: 0.0,
2425 pool_join_s: 0.0,
2426 scan_s: 0.0,
2427 slot_alloc_s: 0.0,
2428 worker_parse_s,
2429 finish_s: 0.0,
2430 worker_sum_s: worker_parse_s,
2431 worker_max_s: worker_parse_s,
2432 chunk_s: vec![worker_parse_s],
2433 scan_kind: SCAN_SERIAL,
2434 scan_minflt: 0,
2435 worker_minflt: vec![worker_flt],
2436 share_frames: vec![n_frames],
2437 share_bytes: vec![bytes.len() as u64],
2438 llc_misses: sample_llc_misses(),
2439 numa_remote: numa_remote_pages(),
2440 pool: None,
2441 });
2442 }
2443 let requested = Some(num_threads.expect("workers"));
2444 let timed = parse_bytes_timed(bytes, requested);
2445 let t_finish = std::time::Instant::now();
2446 let frames = finish_results(timed.pool.as_deref(), timed.slots)?;
2447 let finish_s = t_finish.elapsed().as_secs_f64();
2448 return Ok(PhasedByteTimes {
2449 frames,
2450 pool_s: timed.pool_s,
2451 pool_join_s: timed.pool_join_s,
2452 scan_s: timed.scan_s,
2453 slot_alloc_s: timed.slot_alloc_s,
2454 worker_parse_s: timed.worker_parse_s,
2455 finish_s,
2456 worker_sum_s: timed.worker_sum_s,
2457 worker_max_s: timed.worker_max_s,
2458 chunk_s: timed.chunk_s,
2459 scan_kind: timed.scan_kind,
2460 scan_minflt: timed.scan_minflt,
2461 worker_minflt: timed.worker_minflt,
2462 share_frames: timed.share_frames,
2463 share_bytes: timed.share_bytes,
2464 llc_misses: timed.llc_misses,
2465 numa_remote: timed.numa_remote,
2466 pool: timed.pool,
2467 });
2468 }
2469 #[cfg(not(feature = "parallel"))]
2470 {
2471 let _ = num_threads;
2472 let t_work = std::time::Instant::now();
2473 let mut frames = Vec::new();
2474 for part in ConFrameIterator::from_bytes(bytes) {
2475 frames.push(part?);
2476 }
2477 let worker_parse_s = t_work.elapsed().as_secs_f64();
2478 Ok(PhasedByteTimes {
2479 frames,
2480 pool_s: 0.0,
2481 pool_join_s: 0.0,
2482 scan_s: 0.0,
2483 slot_alloc_s: 0.0,
2484 worker_parse_s,
2485 finish_s: 0.0,
2486 worker_sum_s: worker_parse_s,
2487 worker_max_s: worker_parse_s,
2488 chunk_s: vec![worker_parse_s],
2489 scan_kind: 0,
2490 scan_minflt: 0,
2491 worker_minflt: Vec::new(),
2492 share_frames: Vec::new(),
2493 share_bytes: Vec::new(),
2494 llc_misses: -1,
2495 numa_remote: 0,
2496 })
2497 }
2498}
2499
2500pub fn pack_position_slab(frames: &[types::ConFrame], workers: usize) -> Option<Vec<f64>> {
2506 if frames.is_empty() {
2507 return None;
2508 }
2509 let n_atoms = frames[0].positions.nrows();
2510 if frames
2511 .iter()
2512 .any(|frame| frame.positions.nrows() != n_atoms)
2513 {
2514 return None;
2515 }
2516 let stride = n_atoms.saturating_mul(3);
2517 let total = frames.len().saturating_mul(stride);
2518 let mut values = Vec::<f64>::with_capacity(total);
2519 if total > 0 {
2520 unsafe {
2524 values.set_len(total);
2525 }
2526 }
2527 if stride == 0 {
2528 return Some(values);
2529 }
2530 let write = |frame: &types::ConFrame, dest: &mut [f64]| {
2531 if let Some(src) = frame.positions.f64_slice() {
2532 dest.copy_from_slice(src);
2533 } else {
2534 for (row, slot) in dest.chunks_exact_mut(3).enumerate() {
2535 slot.copy_from_slice(&frame.positions.as_f64_row(row));
2536 }
2537 }
2538 };
2539 #[cfg(feature = "parallel")]
2540 {
2541 if workers >= 2 {
2542 let chunk_frames = frames.len().div_ceil(workers).max(1);
2543 let chunk_vals = chunk_frames.saturating_mul(stride);
2544 let copy_chunks = |dest_all: &mut [f64]| {
2545 use rayon::prelude::*;
2546 dest_all
2547 .par_chunks_mut(chunk_vals)
2548 .enumerate()
2549 .for_each(|(chunk_index, dest)| {
2550 let base = chunk_index * chunk_frames;
2551 for (offset, slot) in dest.chunks_exact_mut(stride).enumerate() {
2552 let index = base + offset;
2553 if index < frames.len() {
2554 write(&frames[index], slot);
2555 }
2556 }
2557 });
2558 };
2559 if rayon::current_thread_index().is_some() {
2563 first_touch_f64(&mut values, workers);
2564 copy_chunks(&mut values);
2565 } else {
2566 first_touch_f64(&mut values, workers);
2567 std::thread::scope(|scope| {
2568 let mut rest = &mut values[..];
2569 let mut base = 0usize;
2570 let mut handles = Vec::new();
2571 while base < frames.len() {
2572 let take_frames = chunk_frames.min(frames.len() - base);
2573 let take_vals = take_frames * stride;
2574 let (dest, tail) = rest.split_at_mut(take_vals);
2575 rest = tail;
2576 let frame_slice = &frames[base..base + take_frames];
2577 handles.push(scope.spawn(move || {
2578 for (frame, slot) in
2579 frame_slice.iter().zip(dest.chunks_exact_mut(stride))
2580 {
2581 write(frame, slot);
2582 }
2583 }));
2584 base += take_frames;
2585 }
2586 for handle in handles {
2587 handle.join().expect("slab worker");
2588 }
2589 });
2590 }
2591 return Some(values);
2592 }
2593 }
2594 #[cfg(not(feature = "parallel"))]
2595 {
2596 let _ = workers;
2597 }
2598 for (frame, dest) in frames.iter().zip(values.chunks_exact_mut(stride)) {
2599 write(frame, dest);
2600 }
2601 Some(values)
2602}
2603
2604fn mix_touch(hash: u64, value: u64) -> u64 {
2605 hash.wrapping_mul(0x9E3779B185EBCA87) ^ value
2606}
2607
2608fn one_frame_touch(frame: &types::ConFrame, coords: &[f64], index: u64) -> u64 {
2609 let mut hash = index;
2610 for value in frame.header.boxl {
2611 hash = mix_touch(hash, value.to_bits());
2612 }
2613 for atom in &frame.atom_data {
2614 hash = mix_touch(hash, atom.symbol.len() as u64);
2615 for byte in atom.symbol.as_bytes() {
2616 hash = mix_touch(hash, u64::from(*byte));
2617 }
2618 }
2619 for value in coords {
2620 hash = mix_touch(hash, value.to_bits());
2621 }
2622 hash
2623}
2624
2625pub fn touch_frame_checksum(
2632 frames: &[types::ConFrame],
2633 positions: &[f64],
2634 workers: usize,
2635) -> Option<u64> {
2636 if frames.is_empty() {
2637 return Some(0);
2638 }
2639 let n_atoms = frames[0].atom_data.len();
2640 let stride = n_atoms.saturating_mul(3);
2641 if stride == 0 || positions.len() != frames.len().saturating_mul(stride) {
2642 return None;
2643 }
2644 if frames.iter().any(|frame| frame.atom_data.len() != n_atoms) {
2645 return None;
2646 }
2647 let one = |index: usize| {
2648 let start = index * stride;
2649 one_frame_touch(
2650 &frames[index],
2651 &positions[start..start + stride],
2652 index as u64,
2653 )
2654 };
2655 Some(reduce_touch(frames.len(), workers, one))
2656}
2657
2658pub fn touch_option_checksum(
2662 frames: &[Option<types::ConFrame>],
2663 positions: &[f64],
2664 workers: usize,
2665) -> Option<u64> {
2666 if frames.is_empty() {
2667 return Some(0);
2668 }
2669 if frames.iter().any(Option::is_none) {
2670 return None;
2671 }
2672 let n_atoms = frames[0].as_ref()?.atom_data.len();
2673 let stride = n_atoms.saturating_mul(3);
2674 if stride == 0 || positions.len() != frames.len().saturating_mul(stride) {
2675 return None;
2676 }
2677 if frames
2678 .iter()
2679 .any(|frame| frame.as_ref().map(|item| item.atom_data.len()) != Some(n_atoms))
2680 {
2681 return None;
2682 }
2683 let one = |index: usize| {
2684 let start = index * stride;
2685 one_frame_touch(
2686 frames[index].as_ref().expect("slot"),
2687 &positions[start..start + stride],
2688 index as u64,
2689 )
2690 };
2691 Some(reduce_touch(frames.len(), workers, one))
2692}
2693
2694fn reduce_touch(n: usize, workers: usize, one: impl Fn(usize) -> u64 + Sync) -> u64 {
2695 #[cfg(feature = "parallel")]
2696 if workers >= 2 {
2697 let fold_range = |first: usize, last: usize| {
2698 let mut acc = 0u64;
2699 for index in first..last {
2700 acc ^= one(index);
2701 }
2702 acc
2703 };
2704 let chunk = n.div_ceil(workers).max(1);
2705 if rayon::current_thread_index().is_some() {
2708 use rayon::prelude::*;
2709 return (0..workers)
2710 .into_par_iter()
2711 .map(|worker| {
2712 let first = worker * chunk;
2713 let last = (first + chunk).min(n);
2714 fold_range(first, last)
2715 })
2716 .reduce(|| 0u64, |left, right| left ^ right);
2717 }
2718 let mut acc = 0u64;
2719 std::thread::scope(|scope| {
2720 let mut handles = Vec::with_capacity(workers);
2721 for worker in 0..workers {
2722 handles.push(scope.spawn(move || {
2723 let first = worker * chunk;
2724 let last = (first + chunk).min(n);
2725 fold_range(first, last)
2726 }));
2727 }
2728 for handle in handles {
2729 acc ^= handle.join().unwrap_or(0);
2730 }
2731 });
2732 return acc;
2733 }
2734 #[cfg(not(feature = "parallel"))]
2735 {
2736 let _ = workers;
2737 }
2738 (0..n).map(one).fold(0, |acc, item| acc ^ item)
2739}
2740
2741pub fn measure_newline_scan(text: &str, threads: usize, chunked: bool) -> (Vec<usize>, f64) {
2749 #[cfg(feature = "parallel")]
2750 {
2751 if chunked && threads >= 2 {
2752 let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build();
2753 let t_scan = std::time::Instant::now();
2754 let offsets = match pool {
2755 Ok(pool) => pool.install(|| {
2756 let newlines = newline_positions(text.as_bytes(), threads);
2757 frame_start_offsets_indexed(text, newlines)
2758 }),
2759 Err(_) => {
2760 let newlines = newline_positions(text.as_bytes(), threads);
2761 frame_start_offsets_indexed(text, newlines)
2762 }
2763 };
2764 return (offsets, t_scan.elapsed().as_secs_f64());
2765 }
2766 }
2767 #[cfg(not(feature = "parallel"))]
2768 {
2769 let _ = (threads, chunked);
2770 }
2771 let t_scan = std::time::Instant::now();
2772 let offsets = frame_start_offsets_serial(text);
2773 (offsets, t_scan.elapsed().as_secs_f64())
2774}
2775
2776#[cfg(all(test, feature = "parallel"))]
2777mod parallel_strong_scale_tests {
2778 use super::*;
2779 use std::path::PathBuf;
2780
2781 fn multi_frame_fixture() -> String {
2782 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con");
2783 let one = std::fs::read_to_string(p).expect("fixture");
2784 one.repeat(8)
2786 }
2787
2788 fn sequential_frames(text: &str) -> Vec<types::ConFrame> {
2789 ConFrameIterator::new(text)
2790 .map(|r| r.expect("seq frame"))
2791 .collect()
2792 }
2793
2794 fn frames_payload_key(f: &types::ConFrame) -> (usize, Vec<(String, f64, f64, f64)>) {
2795 let atoms: Vec<_> = f
2796 .atom_data
2797 .iter()
2798 .map(|a| (a.symbol.to_string(), a.x, a.y, a.z))
2799 .collect();
2800 (f.atom_data.len(), atoms)
2801 }
2802
2803 #[test]
2804 fn parallel_workers_match_sequential_payloads() {
2805 let text = multi_frame_fixture();
2806 let seq = sequential_frames(&text);
2807 assert!(seq.len() >= 8);
2808 let seq_keys: Vec<_> = seq.iter().map(frames_payload_key).collect();
2809
2810 for workers in [1usize, 2, 4] {
2811 let par = parse_frames_parallel_with_threads(&text, Some(workers));
2812 assert_eq!(par.len(), seq.len(), "workers={workers}");
2813 let par_keys: Vec<_> = par
2814 .into_iter()
2815 .map(|r| frames_payload_key(&r.expect("par frame")))
2816 .collect();
2817 assert_eq!(par_keys, seq_keys, "workers={workers} frame payloads");
2818 }
2819
2820 let par_default = parse_frames_parallel(&text);
2822 let def_keys: Vec<_> = par_default
2823 .into_iter()
2824 .map(|r| frames_payload_key(&r.expect("par")))
2825 .collect();
2826 assert_eq!(def_keys, seq_keys);
2827 }
2828
2829 fn live_threads() -> usize {
2830 std::fs::read_to_string("/proc/self/status")
2831 .unwrap()
2832 .lines()
2833 .find(|line| line.starts_with("Threads:"))
2834 .unwrap()
2835 .split_whitespace()
2836 .nth(1)
2837 .unwrap()
2838 .parse()
2839 .unwrap()
2840 }
2841
2842 #[test]
2844 fn default_read_of_a_wide_file_stays_on_the_calling_thread() {
2845 const CHILD: &str = "READCON_DEFAULT_THREAD_CHILD";
2846 const NAME: &str = "iterators::parallel_strong_scale_tests::default_read_of_a_wide_file_stays_on_the_calling_thread";
2847 if std::env::var_os(CHILD).is_none() {
2848 let out = std::process::Command::new(std::env::current_exe().unwrap())
2849 .args([NAME, "--exact", "--test-threads=1"])
2850 .env(CHILD, "1")
2851 .output()
2852 .unwrap();
2853 let stdout = String::from_utf8_lossy(&out.stdout);
2854 assert!(
2855 out.status.success() && stdout.contains("test result: ok. 1 passed;"),
2856 "{stdout}{}",
2857 String::from_utf8_lossy(&out.stderr)
2858 );
2859 return;
2860 }
2861 let dir = tempfile::tempdir().unwrap();
2862 let path = dir.path().join("wide.con");
2863 let one = std::fs::read_to_string(
2864 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con"),
2865 )
2866 .unwrap();
2867 std::fs::write(&path, one.repeat(200)).unwrap();
2868 assert!(std::fs::metadata(&path).unwrap().len() > 64 * 1024);
2869 let before = live_threads();
2870 let frames = super::read_all_frames(&path).unwrap();
2871 let after = live_threads();
2872 assert_eq!(before, after, "default read started worker threads");
2873 assert!(frames.len() > 1);
2874 let text = std::fs::read_to_string(&path).unwrap();
2875 let sequential = super::frames_from_text(&text, None).unwrap();
2876 assert_eq!(sequential.len(), frames.len());
2877 assert_eq!(
2878 sequential[0].atom_data[0].x, frames[0].atom_data[0].x,
2879 "default read changed a coordinate"
2880 );
2881 }
2882}
2883
2884#[cfg(all(test, feature = "parallel"))]
2885mod parallel_header_scan_tests {
2886 use super::*;
2887 use std::path::PathBuf;
2888
2889 fn fixture(name: &str) -> String {
2890 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2891 .join("resources/test")
2892 .join(name);
2893 std::fs::read_to_string(path).expect(name)
2894 }
2895
2896 fn assert_crlf_gap_cut(text: &str) {
2898 let bytes = text.as_bytes();
2899 let mut found = 0usize;
2900 let mut i = 0usize;
2901 while i + 1 < bytes.len() && found < 6 {
2902 if bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
2903 let cut = i + 1;
2904 assert_eq!(bytes[cut - 1], b'\r');
2905 assert_eq!(bytes[cut], b'\n');
2906 assert_cuts_match(text, &[0, cut, bytes.len()]);
2907 found += 1;
2908 i += 2;
2909 continue;
2910 }
2911 i += 1;
2912 }
2913 assert!(found > 0, "fixture has no CRLF pair");
2914 }
2915
2916 fn assert_cuts_match(text: &str, cuts: &[usize]) {
2917 let serial_nl: Vec<usize> = memchr::memchr_iter(b'\n', text.as_bytes()).collect();
2918 let stitched = newline_positions_at_cuts(text.as_bytes(), cuts);
2919 assert_eq!(stitched, serial_nl, "newline stitch at {cuts:?}");
2920 let serial = frame_start_offsets_serial(text);
2921 let parallel = frame_start_offsets_at_cuts(text, cuts);
2922 assert_eq!(parallel, serial, "frame offsets at {cuts:?}");
2923 }
2924
2925 fn header_straddle_cuts(text: &str) -> Vec<Vec<usize>> {
2926 let serial = frame_start_offsets_serial(text);
2927 let bytes = text.as_bytes();
2928 let mut out = Vec::new();
2929 for n in [2usize, 3, 4, 8] {
2931 out.push(equal_cuts(bytes.len(), n));
2932 }
2933 for (index, &start) in serial.iter().enumerate() {
2934 let next = serial.get(index + 1).copied().unwrap_or(bytes.len());
2935 let window = super::header_window_end(bytes, start, next);
2938 let mut line = start;
2939 while line < window {
2940 let Some(rel) = memchr::memchr(b'\n', &bytes[line..window]) else {
2941 break;
2942 };
2943 let end = line + rel;
2944 if end - line > 1 {
2945 let mid = line + (end - line) / 2;
2946 assert!(mid > line && mid < end);
2948 out.push(vec![0, mid, bytes.len()]);
2949 }
2950 line = end + 1;
2951 }
2952 }
2953 out
2954 }
2955
2956 fn assert_slab(text: &str) {
2957 assert!(!text.is_empty());
2958 for cuts in header_straddle_cuts(&text) {
2959 assert_cuts_match(&text, &cuts);
2960 }
2961 let bare = text.trim_end_matches(['\n', '\r']);
2962 if !bare.is_empty() {
2963 assert_cuts_match(bare, &equal_cuts(bare.len(), 4));
2964 let mid = bare.len() / 2;
2965 let line_start = bare[..mid].rfind('\n').map(|i| i + 1).unwrap_or(0);
2966 let line_end = bare[mid..]
2967 .find('\n')
2968 .map(|i| mid + i)
2969 .unwrap_or(bare.len());
2970 if line_end - line_start > 1 {
2971 let cut = line_start + (line_end - line_start) / 2;
2972 assert_cuts_match(bare, &[0, cut, bare.len()]);
2973 }
2974 }
2975 let tailed = format!("{text}\n \n\t\n");
2976 assert_cuts_match(
2977 &tailed,
2978 &[0, 3, tailed.len() / 2, tailed.len() - 1, tailed.len()],
2979 );
2980 let crlf = text.replace('\n', "\r\n");
2981 assert_cuts_match(&crlf, &equal_cuts(crlf.len(), 5));
2982 assert_crlf_gap_cut(&crlf);
2983 let serial = frame_start_offsets_serial(text);
2984 if serial.len() >= 2 {
2985 let second = serial[1];
2986 let next = serial.get(2).copied().unwrap_or(text.len());
2987 let broken_at = second + (next - second) / 2;
2988 let broken = &text[..broken_at.max(1)];
2989 assert_cuts_match(broken, &[0, 1.max(broken.len() / 3), broken.len()]);
2990 }
2991 }
2992
2993 #[test]
2994 fn chunked_header_scan_matches_serial_offsets() {
2995 assert_eq!(frame_start_offsets_serial(""), frame_start_offsets(""));
2996 assert_cuts_match("\n", &[0, 1]);
2997 assert_cuts_match(" \n\n", &[0, 2, 5]);
2998 for name in [
2999 "tiny_cuh2.con",
3000 "tiny_cuh2.convel",
3001 "tiny_cuh2_forces.con",
3002 "tiny_cuh2_vel_forces.con",
3003 "cuh2.con",
3004 ] {
3005 let one = fixture(name);
3006 assert_slab(&one);
3007 assert_slab(&one.repeat(3));
3008 }
3009 let one = fixture("cuh2.con");
3013 for wide in [one.clone(), one.repeat(8)] {
3014 let serial = frame_start_offsets_serial(&wide);
3015 for workers in [2usize, 3, 4, 8] {
3016 let parallel = frame_start_offsets_with_threads(&wide, Some(workers));
3017 assert_eq!(parallel, serial, "workers={workers} bytes={}", wide.len());
3018 let parsed =
3019 frames_from_bytes(wide.as_bytes(), Some(workers)).expect("bytes parse");
3020 assert_eq!(parsed.len(), serial.len());
3021 let seq = frames_from_text(&wide, Some(1)).expect("serial parse");
3022 assert_eq!(parsed.len(), seq.len());
3023 assert_eq!(parsed[0].atom_data[0].x, seq[0].atom_data[0].x);
3024 assert_eq!(
3025 parsed.last().unwrap().atom_data.last().unwrap().z,
3026 seq.last().unwrap().atom_data.last().unwrap().z
3027 );
3028 }
3029 }
3030 }
3031}
3032
3033#[cfg(all(test, feature = "parallel"))]
3038fn header_window_end(bytes: &[u8], start: usize, limit: usize) -> usize {
3039 let limit = limit.min(bytes.len());
3040 let marker = b"Coordinates";
3041 let mut line = start;
3042 while line < limit {
3043 let rest = &bytes[line..limit];
3044 let rel = memchr::memchr(b'\n', rest).unwrap_or(rest.len());
3045 let content_end = line + rel;
3046 let content = &bytes[line..content_end];
3047 let trimmed = match content.iter().position(|byte| !byte.is_ascii_whitespace()) {
3048 Some(index) => &content[index..],
3049 None => b"",
3050 };
3051 if trimmed.starts_with(marker) {
3052 return line;
3053 }
3054 if content_end >= limit {
3055 break;
3056 }
3057 line = content_end + 1;
3058 }
3059 limit
3060}
3061
3062#[cfg(all(test, feature = "parallel"))]
3063mod parallel_frame_parse_tests {
3064 use super::*;
3065 use std::path::PathBuf;
3066
3067 const ZERO_ATOM: &str = "\
3068zero-atom frame
3069{\"con_spec_version\":3,\"con_spec_minor\":1,\"units\":{\"length\":\"angstrom\",\"energy\":\"eV\"}}
307010.0 10.0 10.0
307190.0 90.0 90.0
3072
3073
30740
3075
3076
3077";
3078
3079 fn fixture(name: &str) -> String {
3080 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3081 .join("resources/test")
3082 .join(name);
3083 std::fs::read_to_string(path).expect(name)
3084 }
3085
3086 fn serial_results(text: &str) -> Vec<Result<types::ConFrame, error::ParseError>> {
3087 ConFrameIterator::new(text).collect()
3088 }
3089
3090 fn written(frame: &types::ConFrame) -> Vec<u8> {
3091 let mut out = Vec::new();
3092 {
3093 let mut writer = crate::writer::ConFrameWriter::new(&mut out);
3094 writer.write_frame(frame).expect("write");
3095 writer.flush().expect("flush");
3096 }
3097 out
3098 }
3099
3100 fn assert_same_results(
3101 label: &str,
3102 serial: Vec<Result<types::ConFrame, error::ParseError>>,
3103 parallel: Vec<Result<types::ConFrame, error::ParseError>>,
3104 ) {
3105 assert_eq!(serial.len(), parallel.len(), "{label} count");
3106 for (i, (left, right)) in serial.into_iter().zip(parallel).enumerate() {
3107 match (left, right) {
3108 (Ok(a), Ok(b)) => {
3109 assert_eq!(written(&a), written(&b), "{label} bytes frame {i}");
3110 assert_eq!(a, b, "{label} fields frame {i}");
3111 }
3112 (Err(a), Err(b)) => {
3113 assert_eq!(
3114 format!("{a:?}"),
3115 format!("{b:?}"),
3116 "{label} error frame {i}"
3117 );
3118 }
3119 (Ok(_), Err(err)) => panic!("{label} frame {i} serial ok parallel {err:?}"),
3120 (Err(err), Ok(_)) => panic!("{label} frame {i} serial {err:?} parallel ok"),
3121 }
3122 }
3123 }
3124
3125 fn assert_workers(label: &str, text: &str) {
3126 let serial_ok = serial_results(text).iter().all(|item| item.is_ok());
3127 for workers in [1usize, 2, 3, 4, 8] {
3128 let parallel = parse_frames_parallel_with_threads(text, Some(workers));
3129 assert_same_results(
3130 &format!("{label} workers {workers}"),
3131 serial_results(text),
3132 parallel,
3133 );
3134 let bounds = collect_frame_starts(ConFrameIterator::new(text), true);
3135 let at = parse_frames_at_offsets(text, &bounds, Some(workers));
3136 assert_same_results(
3137 &format!("{label} offsets workers {workers}"),
3138 serial_results(text),
3139 at,
3140 );
3141 }
3142 if serial_ok {
3143 for workers in [2usize, 3, 8] {
3144 let bytes = frames_from_bytes(text.as_bytes(), Some(workers)).expect(label);
3145 let seq = frames_from_text(text, Some(1)).expect(label);
3146 assert_eq!(bytes.len(), seq.len(), "{label} byte count");
3147 for (i, (a, b)) in bytes.iter().zip(seq.iter()).enumerate() {
3148 assert_eq!(written(a), written(b), "{label} byte path frame {i}");
3149 assert_eq!(a, b, "{label} byte path fields frame {i}");
3150 }
3151 }
3152 }
3153 }
3154
3155 fn crlf_gap_offsets(text: &str) -> Vec<usize> {
3156 let bytes = text.as_bytes();
3157 let mut cut = None;
3158 let mut i = 0usize;
3159 while i + 1 < bytes.len() {
3160 if bytes[i] == b'\r' && bytes[i + 1] == b'\n' {
3161 cut = Some(i + 1);
3162 break;
3163 }
3164 i += 1;
3165 }
3166 let cut = cut.expect("crlf");
3167 frame_start_offsets_at_cuts(text, &[0, cut, bytes.len()])
3168 }
3169
3170 #[test]
3171 fn parallel_slots_match_serial_frames() {
3172 assert_workers("empty", "");
3173 assert_workers("blank", "\n");
3174 assert_workers("space", " \n\n");
3175 assert_workers("zero", ZERO_ATOM);
3176 assert_workers("zero-repeat", &ZERO_ATOM.repeat(5));
3177 for name in [
3178 "tiny_cuh2.con",
3179 "tiny_cuh2.convel",
3180 "tiny_cuh2_forces.con",
3181 "tiny_cuh2_vel_forces.con",
3182 "tiny_cuh2_charges_spins_magmoms.con",
3183 "tiny_cuh2_displacements.con",
3184 "tiny_cuh2_spreads.con",
3185 "cuh2.con",
3186 ] {
3187 let one = fixture(name);
3188 assert_workers(name, &one);
3189 assert_workers(&format!("{name} x5"), &one.repeat(5));
3190 let bare = one.trim_end_matches(['\n', '\r']);
3191 assert_workers(&format!("{name} bare"), bare);
3192 let tailed = format!("{one}\n \n\t\n");
3193 assert_workers(&format!("{name} tail"), &tailed);
3194 let crlf = one.replace('\n', "\r\n");
3195 assert_workers(&format!("{name} crlf"), &crlf);
3196 let gap = crlf_gap_offsets(&crlf);
3197 assert_eq!(
3198 gap,
3199 frame_start_offsets_serial(&crlf),
3200 "{name} crlf gap offsets"
3201 );
3202 let parsed = parse_frames_at_offsets(&crlf, &gap, Some(4));
3203 assert_same_results(
3204 &format!("{name} crlf gap parse"),
3205 serial_results(&crlf),
3206 parsed,
3207 );
3208 let serial_offs = frame_start_offsets_serial(&one);
3209 let broken: &str = if serial_offs.len() >= 2 {
3210 let second = serial_offs[1];
3211 let next = serial_offs.get(2).copied().unwrap_or(one.len());
3212 &one[..second + (next - second) / 2]
3213 } else if one.len() > 8 {
3214 &one[..one.len() / 2]
3215 } else {
3216 ""
3217 };
3218 if !broken.is_empty() {
3219 let serial_err = frames_from_text(broken, Some(1))
3220 .err()
3221 .map(|e| format!("{e:?}"));
3222 assert!(serial_err.is_some(), "{name} chopped still parsed");
3223 for workers in [1usize, 2, 4, 8] {
3224 let parallel_err = frames_from_text(broken, Some(workers))
3225 .err()
3226 .map(|e| format!("{e:?}"));
3227 assert_eq!(parallel_err, serial_err, "{name} chopped workers {workers}");
3228 let byte_err = frames_from_bytes(broken.as_bytes(), Some(workers))
3229 .err()
3230 .map(|e| format!("{e:?}"));
3231 assert_eq!(
3232 byte_err, serial_err,
3233 "{name} chopped bytes workers {workers}"
3234 );
3235 }
3236 }
3237 }
3238 }
3239}
3240
3241#[cfg(all(test, feature = "parallel"))]
3242mod phased_read_tests {
3243 use super::*;
3244 use std::path::PathBuf;
3245
3246 fn fixture(name: &str) -> String {
3247 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3248 .join("resources/test")
3249 .join(name);
3250 std::fs::read_to_string(path).expect(name)
3251 }
3252
3253 #[test]
3254 fn phased_bytes_match_serial_frames() {
3255 let text = fixture("cuh2.con").repeat(4);
3256 let serial = frames_from_text(&text, Some(1)).expect("serial");
3257 for workers in [1usize, 2, 4, 8] {
3258 let phased = frames_from_bytes_phased(text.as_bytes(), Some(workers)).expect("phased");
3259 let parallel = phased.frames;
3260 assert_eq!(parallel.len(), serial.len(), "workers={workers}");
3261 assert!(
3262 phased.scan_s >= 0.0 && phased.slot_alloc_s >= 0.0 && phased.worker_parse_s >= 0.0
3263 );
3264 assert_eq!(parallel[0], serial[0], "workers={workers} first");
3265 assert_eq!(
3266 parallel.last().unwrap(),
3267 serial.last().unwrap(),
3268 "workers={workers} last"
3269 );
3270 let slab = pack_position_slab(&serial, workers).expect("slab");
3271 let stride = serial[0].positions.nrows() * 3;
3272 let src = serial[0].positions.f64_slice().expect("f64");
3273 assert_eq!(&slab[..stride], src);
3274 let (chunked, _) = measure_newline_scan(&text, workers.max(2), true);
3275 let (plain, _) = measure_newline_scan(&text, 1, false);
3276 if workers >= 2 {
3277 assert_eq!(chunked, plain);
3278 }
3279 }
3280 }
3281
3282 #[test]
3283 fn phased_file_matches_serial_and_maps_a_wide_file() {
3284 let text = fixture("cuh2.con").repeat(5);
3285 let dir = std::env::temp_dir();
3286 let path = dir.join("readcon-phased-wide.con");
3287 std::fs::write(&path, &text).expect("write");
3288 let phased = read_all_frames_phased(&path, Some(4)).expect("phased");
3289 let serial = frames_from_text(&text, Some(1)).expect("serial");
3290 assert_eq!(phased.frames, serial);
3291 assert!(phased.phases.mapped, "a file past 64 KiB is mapped");
3292 assert!(phased.phases.worker_parse_s >= 0.0);
3293 assert_eq!(phased.n_bytes, text.len());
3294 let _ = std::fs::remove_file(&path);
3295 }
3296
3297 #[test]
3298 fn equal_span_boundaries_match_the_serial_walk() {
3299 let text = fixture("cuh2.con").repeat(8);
3300 let bytes = text.as_bytes();
3301 let got = equal_span_boundaries(bytes, 4).expect("equal span");
3302 let expect = collect_frame_starts(ConFrameIterator::from_bytes(bytes), true);
3303 assert_eq!(got, expect);
3304 assert_eq!(got.len(), 8);
3305 }
3306
3307 #[test]
3308 fn a_whitespace_tail_skips_the_equal_span_walk() {
3309 let mut text = fixture("cuh2.con").repeat(3);
3310 text.push('\n');
3311 assert!(equal_span_boundaries(text.as_bytes(), 4).is_none());
3312 let serial = frames_from_text(&text, Some(1)).expect("serial");
3313 let parallel = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("phased");
3314 assert_eq!(parallel.frames, serial);
3315 }
3316
3317 #[test]
3318 fn touch_checksum_matches_across_worker_counts() {
3319 let text = fixture("cuh2.con").repeat(4);
3320 let serial = frames_from_bytes_phased(text.as_bytes(), Some(1)).expect("serial");
3321 let parallel = frames_from_bytes_phased(text.as_bytes(), Some(4)).expect("parallel");
3322 let slab_s = pack_position_slab(&serial.frames, 1).expect("slab");
3323 let slab_p = pack_position_slab(¶llel.frames, 4).expect("slab");
3324 let left = touch_frame_checksum(&serial.frames, &slab_s, 1).expect("touch");
3325 let right = touch_frame_checksum(¶llel.frames, &slab_p, 4).expect("touch");
3326 assert_eq!(left, right);
3327 assert_ne!(left, 0);
3328 }
3329}