use arrow::array::{Array, ArrayRef, AsArray, BooleanArray};
use arrow::buffer::BooleanBuffer;
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use std::collections::HashMap;
use std::sync::Arc;
fn const_bool_array(n: usize, value: bool) -> ArrayRef {
let buf = if value {
BooleanBuffer::new_set(n)
} else {
BooleanBuffer::new_unset(n)
};
Arc::new(BooleanArray::new(buf, None))
}
struct Bitset {
words: Vec<u64>,
len: usize,
}
impl Bitset {
fn with_capacity(n: usize) -> Self {
Self {
words: vec![0u64; n.div_ceil(64).max(1)],
len: 0,
}
}
fn push(&mut self, val: bool) {
let word_idx = self.len / 64;
if word_idx >= self.words.len() {
self.words.push(0);
}
if val {
self.words[word_idx] |= 1u64 << (self.len % 64);
}
self.len += 1;
}
fn get(&self, i: usize) -> bool {
(self.words[i / 64] >> (i % 64)) & 1 != 0
}
}
pub struct BitspanRef<'a> {
bits: &'a Bitset,
start: usize,
end: usize,
}
impl BitspanRef<'_> {
pub fn get(&self, i: usize) -> bool {
self.bits.get(self.start + i)
}
pub fn len(&self) -> usize {
self.end - self.start
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
#[cfg(test)]
fn to_vec(&self) -> Vec<bool> {
(0..self.len()).map(|i| self.get(i)).collect()
}
}
pub struct ClusterCsr {
offsets: Vec<u32>,
records: Vec<u64>,
is_identical: Bitset,
}
const RIDX_IDENTICAL_BIT: u64 = 1 << 63;
const RIDX_MASK: u64 = !RIDX_IDENTICAL_BIT;
pub(crate) fn pack_ridx_identical(record_idx: u64, is_identical: bool) -> u64 {
debug_assert_eq!(
record_idx & RIDX_IDENTICAL_BIT,
0,
"record_idx must fit in the low 63 bits"
);
record_idx | if is_identical { RIDX_IDENTICAL_BIT } else { 0 }
}
impl ClusterCsr {
pub(crate) fn build(mut pairs: Vec<(u64, u64)>) -> Self {
use rayon::slice::ParallelSliceMut;
pairs.par_sort_unstable_by_key(|&(mk, r)| (mk, r & RIDX_MASK));
let mut offsets = Vec::new();
let mut records = Vec::with_capacity(pairs.len());
let mut is_identical = Bitset::with_capacity(pairs.len());
let mut last_key: Option<u64> = None;
for (mk, r) in pairs {
if last_key != Some(mk) {
offsets.push(records.len() as u32);
last_key = Some(mk);
}
records.push(r & RIDX_MASK);
is_identical.push(r & RIDX_IDENTICAL_BIT != 0);
}
offsets.push(records.len() as u32);
Self {
offsets,
records,
is_identical,
}
}
pub fn groups(&self) -> impl Iterator<Item = (&[u64], BitspanRef<'_>)> {
self.offsets.windows(2).map(move |w| {
let (start, end) = (w[0] as usize, w[1] as usize);
(
&self.records[start..end],
BitspanRef {
bits: &self.is_identical,
start,
end,
},
)
})
}
#[cfg(test)]
fn n_clusters(&self) -> usize {
self.offsets.len().saturating_sub(1)
}
}
pub struct GtResult {
pub n_exact_dup: usize,
pub n_fuzzy_dup: usize,
pub n_hard_neg: usize,
pub n_unique: usize,
pub n_masters: usize,
pub cluster_map: ClusterCsr,
}
fn low_cardinality_dict_type() -> DataType {
DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8))
}
fn draft_schema() -> Arc<Schema> {
Arc::new(Schema::new(vec![
Field::new("record_id", DataType::Utf8, false),
Field::new("master_id", DataType::Utf8, false),
Field::new("entity_type", low_cardinality_dict_type(), false),
Field::new("is_identical", DataType::Boolean, false),
Field::new("is_base", DataType::Boolean, false),
]))
}
pub struct GtAccumulator {
draft_path: String,
writer: arrow::ipc::writer::FileWriter<std::fs::File>,
schema: Arc<Schema>,
dup_masters: rustc_hash::FxHashSet<u64>,
masters_with_exact_copy: rustc_hash::FxHashSet<u64>,
n_base_masters: usize,
}
impl GtAccumulator {
pub(crate) fn dup_masters_len(&self) -> usize {
self.dup_masters.len()
}
pub(crate) fn masters_with_exact_copy_len(&self) -> usize {
self.masters_with_exact_copy.len()
}
pub fn new(draft_path: &str) -> Result<Self, String> {
let schema = draft_schema();
let file = std::fs::File::create(draft_path)
.map_err(|e| format!("create gt draft {draft_path}: {e}"))?;
let writer = arrow::ipc::writer::FileWriter::try_new(file, &schema)
.map_err(|e| format!("gt draft writer: {e}"))?;
Ok(Self {
draft_path: draft_path.to_string(),
writer,
schema,
dup_masters: rustc_hash::FxHashSet::default(),
masters_with_exact_copy: rustc_hash::FxHashSet::default(),
n_base_masters: 0,
})
}
fn write_draft(
&mut self,
record_ids: &ArrayRef,
entity_types: &ArrayRef,
master_ids: &ArrayRef,
is_identical: &ArrayRef,
is_base: &ArrayRef,
) -> Result<(), String> {
let batch = RecordBatch::try_new(
self.schema.clone(),
vec![
record_ids.clone(),
master_ids.clone(),
entity_types.clone(),
is_identical.clone(),
is_base.clone(),
],
)
.map_err(|e| format!("build gt draft batch: {e}"))?;
self.writer
.write(&batch)
.map_err(|e| format!("write gt draft: {e}"))
}
pub fn push_base_batch(
&mut self,
record_ids: &ArrayRef,
entity_types: &ArrayRef,
master_ids: &ArrayRef,
) -> Result<(), String> {
let mids = master_ids.as_string::<i32>();
self.n_base_masters += mids.len() - mids.null_count();
let all_false = const_bool_array(mids.len(), false);
let all_true = const_bool_array(mids.len(), true);
self.write_draft(record_ids, entity_types, master_ids, &all_false, &all_true)
}
pub fn push_dup_batch(
&mut self,
record_ids: &ArrayRef,
entity_types: &ArrayRef,
master_ids: &ArrayRef,
is_identical: &ArrayRef,
) -> Result<(), String> {
let mids = master_ids.as_string::<i32>();
let idents = is_identical.as_boolean();
for i in 0..mids.len() {
if !mids.is_null(i) {
let mid = mids.value(i);
let key = crate::pipeline::pack_master_key(mid).unwrap_or_else(|| {
panic!(
"push_dup_batch: master_id {mid:?} isn't the fixed \
\"{{entity_prefix}}-{{pad_string}}\" shape -- \
push_dup_batch never receives an HN-/CANARY- master_id"
)
});
self.dup_masters.insert(key);
if !idents.is_null(i) && idents.value(i) {
self.masters_with_exact_copy.insert(key);
}
}
}
let all_false = const_bool_array(mids.len(), false);
self.write_draft(
record_ids,
entity_types,
master_ids,
is_identical,
&all_false,
)
}
pub fn push_other_batch(
&mut self,
record_ids: &ArrayRef,
entity_types: &ArrayRef,
master_ids: &ArrayRef,
) -> Result<(), String> {
let all_false = const_bool_array(record_ids.len(), false);
self.write_draft(record_ids, entity_types, master_ids, &all_false, &all_false)
}
pub fn finish(
self,
difficulty: &str,
output_format: &str,
final_path: &str,
metadata: &HashMap<String, String>,
track_clusters: bool,
) -> Result<GtResult, String> {
let GtAccumulator {
draft_path,
mut writer,
dup_masters,
masters_with_exact_copy,
n_base_masters,
..
} = self;
writer
.finish()
.map_err(|e| format!("finish gt draft: {e}"))?;
drop(writer);
let final_schema = Arc::new(
Schema::new(vec![
Field::new("record_id", DataType::Utf8, false),
Field::new("master_id", DataType::Utf8, false),
Field::new("entity_type", low_cardinality_dict_type(), false),
Field::new("match_type", low_cardinality_dict_type(), false),
Field::new("difficulty", low_cardinality_dict_type(), false),
])
.with_metadata(metadata.clone()),
);
let draft_file = std::fs::File::open(&draft_path)
.map_err(|e| format!("reopen gt draft {draft_path}: {e}"))?;
let reader = arrow::ipc::reader::FileReader::try_new(draft_file, None)
.map_err(|e| format!("gt draft reader: {e}"))?;
let mut sink = GtSink::new(output_format, final_path, &final_schema, metadata)?;
let mut n_exact_dup = 0usize;
let mut n_fuzzy_dup = 0usize;
let mut n_hard_neg = 0usize;
let mut n_unique = 0usize;
let mut cluster_pairs: Vec<(u64, u64)> = Vec::new();
let mut diff_arr_cache: Option<(usize, ArrayRef)> = None;
let match_type_dict = crate::pipeline::DictValues::new([
"hard_neg",
"canary",
"exact_dup",
"fuzzy_dup",
"unique",
]);
let difficulty_dict = crate::pipeline::DictValues::new([difficulty.to_string()]);
let k_hard_neg = match_type_dict.key("hard_neg");
let k_canary = match_type_dict.key("canary");
let k_exact_dup = match_type_dict.key("exact_dup");
let k_fuzzy_dup = match_type_dict.key("fuzzy_dup");
let k_unique = match_type_dict.key("unique");
for batch_result in reader {
let batch = batch_result.map_err(|e| format!("read gt draft batch: {e}"))?;
let n = batch.num_rows();
let mid_col = batch.column(1).as_string::<i32>();
let rid_col = batch.column(0).as_string::<i32>();
let ident_col = batch.column(3).as_boolean();
let base_col = batch.column(4).as_boolean();
let mut mt_keys = arrow::array::Int32Builder::with_capacity(n);
for i in 0..n {
let mid = if mid_col.is_null(i) {
""
} else {
mid_col.value(i)
};
let is_base = !base_col.is_null(i) && base_col.value(i);
let is_hn = mid.starts_with("HN-");
let is_canary = !is_hn && mid.starts_with("CANARY-");
let master_key = if is_hn || is_canary {
None
} else {
crate::pipeline::pack_master_key(mid)
};
let is_identical = if is_base {
master_key.is_some_and(|k| masters_with_exact_copy.contains(&k))
} else {
!ident_col.is_null(i) && ident_col.value(i)
};
let is_dup_master = master_key.is_some_and(|k| dup_masters.contains(&k));
let mt_key = if is_hn {
n_hard_neg += 1;
k_hard_neg
} else if is_canary {
k_canary
} else if is_dup_master {
if is_identical {
n_exact_dup += 1;
k_exact_dup
} else {
n_fuzzy_dup += 1;
k_fuzzy_dup
}
} else {
n_unique += 1;
k_unique
};
mt_keys.append_value(mt_key);
if track_clusters && is_dup_master {
let master_key = master_key.expect("is_dup_master implies master_key is Some");
let record_idx = crate::pipeline::parse_record_idx(rid_col.value(i))
.unwrap_or_else(|| {
panic!(
"cluster_map: record_id {:?} isn't the fixed \
\"R-\" + 13-digit shape produced by \
`record_id_string`",
rid_col.value(i)
)
});
cluster_pairs.push((master_key, pack_ridx_identical(record_idx, is_identical)));
}
}
let mt_arr = match_type_dict.finish_keys(mt_keys.finish());
let diff_arr = match &diff_arr_cache {
Some((cached_n, arr)) if *cached_n == n => arr.clone(),
_ => {
let arr = difficulty_dict.const_array(difficulty, n);
diff_arr_cache = Some((n, arr.clone()));
arr
}
};
let final_batch = RecordBatch::try_new(
final_schema.clone(),
vec![
batch.column(0).clone(),
batch.column(1).clone(),
batch.column(2).clone(),
mt_arr,
diff_arr,
],
)
.map_err(|e| format!("build final gt batch: {e}"))?;
sink.write(&final_batch)?;
}
sink.finish()?;
std::fs::remove_file(&draft_path).ok();
Ok(GtResult {
n_exact_dup,
n_fuzzy_dup,
n_hard_neg,
n_unique,
n_masters: n_base_masters,
cluster_map: ClusterCsr::build(cluster_pairs),
})
}
}
enum GtSink {
Ipc(Box<arrow::ipc::writer::FileWriter<std::fs::File>>),
Parquet(Box<parquet::arrow::ArrowWriter<std::fs::File>>),
}
impl GtSink {
fn new(
output_format: &str,
path: &str,
schema: &Arc<Schema>,
metadata: &HashMap<String, String>,
) -> Result<Self, String> {
let file = std::fs::File::create(path).map_err(|e| format!("create {path}: {e}"))?;
if output_format == "parquet" {
use parquet::basic::{Compression, ZstdLevel};
use parquet::file::properties::WriterProperties;
let zstd = ZstdLevel::try_new(3).map_err(|e| format!("zstd: {e}"))?;
let meta_kv: Vec<parquet::file::metadata::KeyValue> = metadata
.iter()
.map(|(k, v)| parquet::file::metadata::KeyValue {
key: k.clone(),
value: Some(v.clone()),
})
.collect();
let props = WriterProperties::builder()
.set_compression(Compression::ZSTD(zstd))
.set_data_page_size_limit(1_048_576)
.set_key_value_metadata(Some(meta_kv))
.build();
let writer = parquet::arrow::ArrowWriter::try_new(file, schema.clone(), Some(props))
.map_err(|e| format!("gt parquet writer: {e}"))?;
Ok(GtSink::Parquet(Box::new(writer)))
} else {
let writer = arrow::ipc::writer::FileWriter::try_new(file, schema)
.map_err(|e| format!("gt ipc writer: {e}"))?;
Ok(GtSink::Ipc(Box::new(writer)))
}
}
fn write(&mut self, batch: &RecordBatch) -> Result<(), String> {
match self {
GtSink::Ipc(w) => w.write(batch).map_err(|e| format!("write gt ipc: {e}")),
GtSink::Parquet(w) => w.write(batch).map_err(|e| format!("write gt parquet: {e}")),
}
}
fn finish(self) -> Result<(), String> {
match self {
GtSink::Ipc(mut w) => w.finish().map_err(|e| format!("finish gt ipc: {e}")),
GtSink::Parquet(w) => w
.close()
.map(|_| ())
.map_err(|e| format!("close gt parquet: {e}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::StringArray;
fn tmp_path(name: &str) -> String {
let mut p = std::env::temp_dir();
p.push(format!(
"dupehell_gt_test_{name}_{}.ipc",
std::process::id()
));
p.to_string_lossy().into_owned()
}
fn arr(values: Vec<&str>) -> ArrayRef {
Arc::new(StringArray::from(values))
}
fn arr_owned(values: Vec<String>) -> ArrayRef {
Arc::new(StringArray::from(values))
}
fn barr(values: Vec<bool>) -> ArrayRef {
Arc::new(BooleanArray::from(values))
}
fn dict_arr(dict: &crate::pipeline::DictValues, values: Vec<&str>) -> ArrayRef {
let keys =
arrow::array::Int32Array::from(values.iter().map(|v| dict.key(v)).collect::<Vec<_>>());
dict.finish_keys(keys)
}
fn read_match_types(final_path: &str) -> HashMap<String, String> {
let file = std::fs::File::open(final_path).unwrap();
let reader = arrow::ipc::reader::FileReader::try_new(file, None).unwrap();
let mut out = HashMap::new();
for batch in reader {
let batch = batch.unwrap();
let rid = batch
.column_by_name("record_id")
.unwrap()
.as_string::<i32>();
let mt_col =
arrow::compute::cast(batch.column_by_name("match_type").unwrap(), &DataType::Utf8)
.unwrap();
let mt = mt_col.as_string::<i32>();
for i in 0..batch.num_rows() {
out.insert(rid.value(i).to_string(), mt.value(i).to_string());
}
}
out
}
fn mid(n: usize) -> String {
format!(
"{}-{}",
crate::pipeline::entity_prefix(0),
crate::pipeline::pad_string(n)
)
}
fn rid(i: usize) -> String {
crate::pipeline::record_id_string(i)
}
#[test]
fn test_gt_accumulator_basic() {
let draft = tmp_path("basic_draft");
let final_path = tmp_path("basic_final");
let mut acc = GtAccumulator::new(&draft).unwrap();
let et_dict = crate::pipeline::DictValues::new(["person"]);
acc.push_base_batch(
&arr_owned(vec![rid(1), rid(3), rid(6)]),
&dict_arr(&et_dict, vec!["person", "person", "person"]),
&arr_owned(vec![mid(1), mid(2), mid(5)]),
)
.unwrap();
acc.push_dup_batch(
&arr_owned(vec![rid(2)]),
&dict_arr(&et_dict, vec!["person"]),
&arr_owned(vec![mid(1)]),
&barr(vec![true]),
)
.unwrap();
acc.push_other_batch(
&arr_owned(vec![rid(4), rid(5)]),
&dict_arr(&et_dict, vec!["person", "person"]),
&arr(vec!["HN-0000003", "HN-0000004"]),
)
.unwrap();
let GtResult {
n_exact_dup: ed,
n_hard_neg: hn,
n_unique: un,
n_masters: masters,
..
} = acc
.finish("medium", "ipc", &final_path, &HashMap::new(), false)
.unwrap();
assert_eq!(ed, 2);
assert_eq!(hn, 2);
assert_eq!(un, 2);
assert_eq!(masters, 3);
let match_types = read_match_types(&final_path);
assert_eq!(match_types[&rid(1)], "exact_dup");
assert_eq!(match_types[&rid(2)], "exact_dup");
assert_eq!(match_types[&rid(3)], "unique");
assert_eq!(match_types[&rid(4)], "hard_neg");
assert_eq!(match_types[&rid(5)], "hard_neg");
assert_eq!(match_types[&rid(6)], "unique");
std::fs::remove_file(&final_path).ok();
}
#[test]
fn test_gt_accumulator_fuzzy_dup() {
let draft = tmp_path("fuzzy_draft");
let final_path = tmp_path("fuzzy_final");
let mut acc = GtAccumulator::new(&draft).unwrap();
let et_dict = crate::pipeline::DictValues::new(["person"]);
acc.push_base_batch(
&arr_owned(vec![rid(1)]),
&dict_arr(&et_dict, vec!["person"]),
&arr_owned(vec![mid(1)]),
)
.unwrap();
acc.push_dup_batch(
&arr_owned(vec![rid(2), rid(3)]),
&dict_arr(&et_dict, vec!["person", "person"]),
&arr_owned(vec![mid(1), mid(1)]),
&barr(vec![true, false]),
)
.unwrap();
let GtResult {
n_exact_dup: ed,
n_fuzzy_dup: fd,
n_unique: un,
n_masters: masters,
..
} = acc
.finish("medium", "ipc", &final_path, &HashMap::new(), false)
.unwrap();
assert_eq!(ed, 2); assert_eq!(fd, 1); assert_eq!(un, 0);
assert_eq!(masters, 1);
let match_types = read_match_types(&final_path);
assert_eq!(match_types[&rid(1)], "exact_dup");
assert_eq!(match_types[&rid(2)], "exact_dup");
assert_eq!(match_types[&rid(3)], "fuzzy_dup");
std::fs::remove_file(&final_path).ok();
}
#[test]
fn test_gt_accumulator_multi_batch() {
let draft = tmp_path("multi_draft");
let final_path = tmp_path("multi_final");
let mut acc = GtAccumulator::new(&draft).unwrap();
let et_dict = crate::pipeline::DictValues::new(["person"]);
acc.push_base_batch(
&arr_owned(vec![rid(1), rid(3)]),
&dict_arr(&et_dict, vec!["person", "person"]),
&arr_owned(vec![mid(1), mid(2)]),
)
.unwrap();
acc.push_dup_batch(
&arr_owned(vec![rid(2)]),
&dict_arr(&et_dict, vec!["person"]),
&arr_owned(vec![mid(1)]),
&barr(vec![true]),
)
.unwrap();
let GtResult {
n_exact_dup: ed,
n_hard_neg: hn,
n_unique: un,
n_masters: masters,
..
} = acc
.finish("medium", "ipc", &final_path, &HashMap::new(), false)
.unwrap();
assert_eq!(ed, 2); assert_eq!(hn, 0);
assert_eq!(un, 1);
assert_eq!(masters, 2);
std::fs::remove_file(&final_path).ok();
}
#[test]
fn test_no_cross_entity_suffix_collision() {
let draft = tmp_path("suffix_draft");
let final_path = tmp_path("suffix_final");
let mut acc = GtAccumulator::new(&draft).unwrap();
let et_dict = crate::pipeline::DictValues::new(["person", "account"]);
acc.push_base_batch(
&arr(vec!["R1", "R2"]),
&dict_arr(&et_dict, vec!["person", "account"]),
&arr(vec!["PERSON-0000001", "ACCOUNT-0000001"]),
)
.unwrap();
let GtResult {
n_exact_dup: ed,
n_unique: un,
n_masters: masters,
..
} = acc
.finish("medium", "ipc", &final_path, &HashMap::new(), false)
.unwrap();
assert_eq!(ed, 0);
assert_eq!(un, 2);
assert_eq!(masters, 2);
std::fs::remove_file(&final_path).ok();
}
#[test]
fn test_cluster_map_contents() {
let draft = tmp_path("cm_draft");
let final_path = tmp_path("cm_final");
let mut acc = GtAccumulator::new(&draft).unwrap();
let et_dict = crate::pipeline::DictValues::new(["person"]);
acc.push_base_batch(
&arr_owned(vec![rid(1), rid(3), rid(6)]),
&dict_arr(&et_dict, vec!["person", "person", "person"]),
&arr_owned(vec![mid(1), mid(2), mid(5)]),
)
.unwrap();
acc.push_dup_batch(
&arr_owned(vec![rid(2), rid(7)]),
&dict_arr(&et_dict, vec!["person", "person"]),
&arr_owned(vec![mid(1), mid(5)]),
&barr(vec![true, false]),
)
.unwrap();
acc.push_other_batch(
&arr_owned(vec![rid(4), rid(5)]),
&dict_arr(&et_dict, vec!["person", "person"]),
&arr(vec!["HN-0000003", "HN-0000004"]),
)
.unwrap();
let GtResult {
cluster_map: cm, ..
} = acc
.finish("medium", "ipc", &final_path, &HashMap::new(), true)
.unwrap();
assert_eq!(cm.n_clusters(), 2);
let mut groups = cm.groups();
let (records1, idents1) = groups.next().unwrap();
assert_eq!(records1, &[1, 2]);
assert_eq!(idents1.to_vec(), vec![true, true]);
let (records5, idents5) = groups.next().unwrap();
assert_eq!(records5, &[6, 7]);
assert_eq!(idents5.to_vec(), vec![false, false]);
assert!(groups.next().is_none());
std::fs::remove_file(&final_path).ok();
}
#[test]
#[ignore]
fn hunt_ram_dup_masters_vs_combined_map() {
fn rss_mb() -> f64 {
let pid = sysinfo::Pid::from_u32(std::process::id());
let mut sys = sysinfo::System::new();
sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
sys.process(pid)
.map(|p| p.memory() as f64 / 1e6)
.unwrap_or(0.0)
}
const N_DUP_MASTERS: usize = 15_811_430;
const N_EXACT_COPY: usize = (N_DUP_MASTERS as f64 * 0.70) as usize;
fn mix(i: u64) -> u64 {
let mut z = i.wrapping_add(0x9E3779B97F4A7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
}
let rss0 = rss_mb();
let mut dup_masters: rustc_hash::FxHashSet<u64> =
rustc_hash::FxHashSet::with_capacity_and_hasher(N_DUP_MASTERS, Default::default());
for i in 0..N_DUP_MASTERS as u64 {
dup_masters.insert(mix(i));
}
let mut masters_with_exact_copy: rustc_hash::FxHashSet<u64> =
rustc_hash::FxHashSet::with_capacity_and_hasher(N_EXACT_COPY, Default::default());
for i in 0..N_EXACT_COPY as u64 {
masters_with_exact_copy.insert(mix(i));
}
let rss_two_sets = rss_mb();
println!(
"[hunt_ram] two FxHashSet<u64> ({} + {} entries): rss delta = {:.1} Mo (rss0={:.1} -> {:.1})",
dup_masters.len(),
masters_with_exact_copy.len(),
rss_two_sets - rss0,
rss0,
rss_two_sets
);
drop(dup_masters);
drop(masters_with_exact_copy);
let rss1 = rss_mb();
let mut combined: rustc_hash::FxHashMap<u64, bool> =
rustc_hash::FxHashMap::with_capacity_and_hasher(N_DUP_MASTERS, Default::default());
for i in 0..N_DUP_MASTERS as u64 {
combined.insert(mix(i), i < N_EXACT_COPY as u64);
}
let rss_combined = rss_mb();
println!(
"[hunt_ram] one FxHashMap<u64,bool> ({} entries): rss delta = {:.1} Mo (rss1={:.1} -> {:.1})",
combined.len(),
rss_combined - rss1,
rss1,
rss_combined
);
drop(combined);
let delta_two = rss_two_sets - rss0;
let delta_one = rss_combined - rss1;
println!(
"[hunt_ram] combined map saves {:.1} Mo ({:.1}%) vs two separate sets, at this cardinality",
delta_two - delta_one,
100.0 * (delta_two - delta_one) / delta_two.max(1.0)
);
}
}