use ifc_lite_core::{EntityIndex, EntityScanner};
pub type ShardRecords = Vec<(u32, usize, usize)>;
pub fn scan_shard(
content: &[u8],
range_start: usize,
range_end: usize,
) -> (ShardRecords, Option<usize>) {
let mut scanner = if range_start == 0 {
EntityScanner::new(content)
} else {
EntityScanner::new_at(content, range_start)
};
let mut records = Vec::new();
let mut handoff = None;
while let Some((id, _type_name, start, entity_end)) = scanner.next_entity() {
if start >= range_end {
handoff = Some(start);
break;
}
records.push((id, start, entity_end));
}
(records, handoff)
}
pub fn build_entity_index_parallel<T>(content: &T) -> EntityIndex
where
T: AsRef<[u8]> + ?Sized,
{
let content = content.as_ref();
#[cfg(target_arch = "wasm32")]
{
ifc_lite_core::build_entity_index(content)
}
#[cfg(not(target_arch = "wasm32"))]
{
native::build(content)
}
}
#[cfg(not(target_arch = "wasm32"))]
mod native {
use ifc_lite_core::{build_entity_index, EntityIndex, EntityScanner};
use rayon::prelude::*;
use rustc_hash::FxHashMap;
const PARALLEL_MIN_BYTES: usize = 8 * 1024 * 1024;
const MIN_CHUNK_BYTES: usize = 2 * 1024 * 1024;
pub(super) fn build(content: &[u8]) -> EntityIndex {
let n = chunk_count(content.len());
if n <= 1 {
return build_entity_index(content);
}
with_chunks(content, n)
}
fn chunk_count(len: usize) -> usize {
if len < PARALLEL_MIN_BYTES {
return 1;
}
let threads = rayon::current_num_threads().max(1);
let by_size = (len / MIN_CHUNK_BYTES).max(1);
threads.min(by_size)
}
struct ChunkScan {
records: Vec<(u32, usize, usize)>,
handoff: Option<usize>,
}
#[inline]
fn range_end(i: usize, n_chunks: usize, len: usize) -> usize {
if i + 1 == n_chunks {
len
} else {
(i + 1) * len / n_chunks
}
}
fn scan_chunk(content: &[u8], i: usize, n_chunks: usize) -> ChunkScan {
let start = i * content.len() / n_chunks;
let end = range_end(i, n_chunks, content.len());
let (records, handoff) = super::scan_shard(content, start, end);
ChunkScan { records, handoff }
}
pub(super) fn with_chunks(content: &[u8], n_chunks: usize) -> EntityIndex {
let len = content.len();
let n_chunks = n_chunks.max(1).min(len.max(1));
if n_chunks == 1 {
return build_entity_index(content);
}
let chunks: Vec<ChunkScan> = (0..n_chunks)
.into_par_iter()
.map(|i| scan_chunk(content, i, n_chunks))
.collect();
stitch(content, &chunks, n_chunks)
}
fn stitch(content: &[u8], chunks: &[ChunkScan], n_chunks: usize) -> EntityIndex {
let len = content.len();
let mut index: EntityIndex =
FxHashMap::with_capacity_and_hasher(len / 50, Default::default());
for &(id, start, end) in &chunks[0].records {
index.insert(id, (start, end));
}
let mut expected_start = chunks[0].handoff;
for (i, chunk) in chunks.iter().enumerate().skip(1) {
let target = match expected_start {
Some(t) => t,
None => break,
};
let end = range_end(i, n_chunks, len);
let recs = &chunk.records;
match recs.binary_search_by(|&(_, start, _)| start.cmp(&target)) {
Ok(p) => {
for &(id, start, e) in &recs[p..] {
index.insert(id, (start, e));
}
expected_start = chunk.handoff;
}
Err(_) => {
expected_start = rescan_range(content, target, end, &mut index);
}
}
}
index
}
fn rescan_range(
content: &[u8],
target: usize,
end: usize,
index: &mut EntityIndex,
) -> Option<usize> {
let mut scanner = EntityScanner::new_at(content, target);
while let Some((id, _type_name, start, entity_end)) = scanner.next_entity() {
if start >= end {
return Some(start);
}
index.insert(id, (start, entity_end));
}
None
}
#[cfg(test)]
mod tests {
use super::with_chunks;
use ifc_lite_core::build_entity_index;
fn assert_parallel_matches_serial(content: &[u8], label: &str) {
let serial = build_entity_index(content);
for n in [1usize, 2, 3, 4, 5, 7, 8, 11, 16, 32, 64] {
let par = with_chunks(content, n);
assert_eq!(
par, serial,
"parallel index (n_chunks={n}) != serial for {label}"
);
}
}
#[test]
fn empty_and_tiny_and_malformed() {
assert_parallel_matches_serial(b"", "empty");
assert_parallel_matches_serial(b"\n", "single-newline");
assert_parallel_matches_serial(
b"ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\nENDSEC;\n",
"header-only",
);
assert_parallel_matches_serial(
b"#1=IFCWALL('g',$,$,$,$,$,$,$);\n",
"no-header",
);
assert_parallel_matches_serial(
b"DATA;\n#1=IFCWALL('g',$,$\n#2=IFCDOOR( #notanid #=x ; ;",
"malformed",
);
}
#[test]
fn simple_data_section() {
let mut content = String::from("ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\n");
for id in 1..=200u32 {
content.push_str(&format!(
"#{id}=IFCCARTESIANPOINT(({}.,{}.,{}.));\n",
id, id, id
));
}
content.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
assert_parallel_matches_serial(content.as_bytes(), "simple-200");
}
#[test]
fn duplicate_ids_last_wins() {
let mut content = String::from("DATA;\n");
for _ in 0..3 {
for id in 1..=50u32 {
content.push_str(&format!("#{id}=IFCWALL('g{id}',$,$,$,$,$,$,$);\n"));
}
}
assert_parallel_matches_serial(content.as_bytes(), "duplicate-ids");
}
#[test]
fn chunk_boundary_inside_quoted_string() {
let mut fake = String::new();
for k in 0..400 {
fake.push_str(&format!(";\\n#{}=IFCWALL(fake ; still in string ", 90000 + k));
}
let mut content = String::from("ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\n");
content.push_str("#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);\n");
content.push_str(&format!("#2=IFCWALL('{fake}',$,$,$,$,$,$,$);\n"));
for id in 3..=120u32 {
content.push_str(&format!("#{id}=IFCDOOR('g{id}',$,$,$,$,$,$,$);\n"));
}
content.push_str("ENDSEC;\n");
assert_parallel_matches_serial(content.as_bytes(), "in-string-boundary");
}
#[test]
fn record_larger_than_chunk() {
let big_name = "X".repeat(20_000);
let mut content = String::from("DATA;\n");
content.push_str("#1=IFCPROJECT('g',$,$,$,$,$,$,$,$);\n");
content.push_str(&format!("#2=IFCWALL('{big_name}',$,$,$,$,$,$,$);\n"));
for id in 3..=40u32 {
content.push_str(&format!("#{id}=IFCDOOR('g{id}',$,$,$,$,$,$,$);\n"));
}
assert_parallel_matches_serial(content.as_bytes(), "record-larger-than-chunk");
}
#[test]
fn fixtures_byte_identical() {
for rel in [
"ara3d/schependomlaan.ifc",
"ara3d/AC-20-Smiley-West-10-Bldg.ifc",
"various/01_BIMcollab_Example_ARC.ifc",
] {
let path = format!("{}/../../tests/models/{}", env!("CARGO_MANIFEST_DIR"), rel);
let Ok(content) = std::fs::read(&path) else {
eprintln!("skipping {rel}: fixture absent — run `pnpm fixtures`");
continue;
};
assert_parallel_matches_serial(&content, rel);
assert_eq!(
super::super::build_entity_index_parallel(&content),
build_entity_index(&content),
"public build_entity_index_parallel != serial for {rel}"
);
}
}
}
}