use crate::error::{ParcodeError, Result};
use crate::format::ChildRef;
use crate::graph::{ChunkId, JobConfig, SerializationJob, TaskGraph};
use crate::map::{MapShardJob, hash_key};
use crate::visitor::ParcodeVisitor;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::Hash;
const TARGET_SHARD_SIZE_BYTES: u64 = 128 * 1024;
const MIN_SHARD_SIZE_BYTES: u64 = 4 * 1024;
const TASKS_PER_CORE: usize = 4;
#[derive(Clone, Serialize, Deserialize, Debug)]
struct ShardRun {
item_count: u32,
repeat: u32,
}
#[derive(Clone)]
struct VecContainerJob {
shard_runs: Vec<ShardRun>,
total_items: u64,
}
impl<'a> SerializationJob<'a> for VecContainerJob {
fn execute(&self, _children_refs: &[ChildRef]) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
buffer.extend_from_slice(&self.total_items.to_le_bytes());
let runs_bytes =
bincode::serde::encode_to_vec(&self.shard_runs, bincode::config::standard())
.map_err(|e| ParcodeError::Serialization(e.to_string()))?;
buffer.extend_from_slice(&runs_bytes);
Ok(buffer)
}
fn estimated_size(&self) -> usize {
8 + (self.shard_runs.len() * 8)
}
}
#[derive(Clone)]
struct VecShardJob<'a, T> {
data: &'a [T],
}
impl<'a, T> SerializationJob<'a> for VecShardJob<'a, T>
where
T: ParcodeVisitor + Serialize + Send + Sync + 'static,
{
fn execute(&self, _children_refs: &[ChildRef]) -> Result<Vec<u8>> {
let mut buffer = Vec::<u8>::new();
T::serialize_slice(self.data, &mut buffer)?;
Ok(buffer)
}
fn estimated_size(&self) -> usize {
std::mem::size_of_val(self.data)
}
}
impl<'a, T> SerializationJob<'a> for &T
where
T: SerializationJob<'a> + ?Sized,
{
fn execute(&self, children_refs: &[ChildRef]) -> Result<Vec<u8>> {
(**self).execute(children_refs)
}
fn estimated_size(&self) -> usize {
(**self).estimated_size()
}
fn config(&self) -> JobConfig {
(**self).config()
}
}
impl<T> ParcodeVisitor for Vec<T>
where
T: ParcodeVisitor + Clone + Send + Sync + 'static + Serialize,
{
fn visit<'a>(
&'a self,
graph: &mut TaskGraph<'a>,
parent_id: Option<ChunkId>,
config_override: Option<JobConfig>,
) {
let total_len = self.len();
let items_per_shard;
if total_len == 0 {
items_per_shard = 1;
} else {
let sample_count = total_len.min(8);
let sample_slice = self.get(0..sample_count).unwrap_or(&[]);
let sample_size_bytes =
match bincode::serde::encode_to_vec(sample_slice, bincode::config::standard()) {
Ok(vec) => vec.len() as u64,
Err(_) => 0,
};
let avg_item_size = if sample_size_bytes > 0 {
(sample_size_bytes / sample_count as u64).max(1)
} else {
(std::mem::size_of::<T>() as u64).max(1)
};
let count_by_io = usize::try_from((TARGET_SHARD_SIZE_BYTES / avg_item_size).max(1))
.unwrap_or(usize::MAX);
let num_cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
let target_parallel_chunks = num_cpus * TASKS_PER_CORE;
let count_by_cpu = (total_len / target_parallel_chunks).max(1);
let candidate_count = count_by_io.min(count_by_cpu);
let estimated_chunk_size = candidate_count as u64 * avg_item_size;
if estimated_chunk_size < MIN_SHARD_SIZE_BYTES {
items_per_shard = usize::try_from((MIN_SHARD_SIZE_BYTES / avg_item_size).max(1))
.unwrap_or(usize::MAX);
} else {
items_per_shard = candidate_count;
}
}
let chunks: Vec<&[T]> = self.chunks(items_per_shard).collect();
let mut shard_runs: Vec<ShardRun> = Vec::new();
if !chunks.is_empty() {
let mut current_run = ShardRun {
item_count: u32::try_from(chunks.first().map(|c| c.len()).unwrap_or(0))
.unwrap_or(u32::MAX),
repeat: 0,
};
for chunk in &chunks {
let len = u32::try_from(chunk.len()).unwrap_or(u32::MAX);
if len == current_run.item_count {
current_run.repeat += 1;
} else {
shard_runs.push(current_run);
current_run = ShardRun {
item_count: len,
repeat: 1,
};
}
}
shard_runs.push(current_run);
}
let container_inner = Box::new(VecContainerJob {
shard_runs,
total_items: self.len() as u64,
});
let container_job: Box<dyn SerializationJob<'a> + 'a> = if let Some(cfg) = config_override {
Box::new(crate::rt::ConfiguredJob::new(container_inner, cfg))
} else {
container_inner
};
let my_id = graph.add_node(container_job);
if let Some(pid) = parent_id {
graph.link_parent_child(pid, my_id);
}
if self.is_empty() {
return;
}
for chunk_slice in chunks {
let shard_inner = Box::new(VecShardJob { data: chunk_slice });
let shard_job: Box<dyn SerializationJob<'a> + 'a> = if let Some(cfg) = config_override {
Box::new(crate::rt::ConfiguredJob::new(shard_inner, cfg))
} else {
shard_inner
};
let shard_id = graph.add_node(shard_job);
graph.link_parent_child(my_id, shard_id);
for item in chunk_slice {
item.visit_inlined(graph, shard_id, None);
}
}
}
fn create_job<'a>(
&'a self,
config_override: Option<JobConfig>,
) -> Box<dyn SerializationJob<'a> + 'a> {
let inner = Box::new(VecContainerJob {
shard_runs: Vec::new(),
total_items: 0,
});
if let Some(cfg) = config_override {
Box::new(crate::rt::ConfiguredJob::new(inner, cfg))
} else {
inner
}
}
}
#[derive(Clone)]
struct PrimitiveJob<T>(T);
impl<'a, T> SerializationJob<'a> for PrimitiveJob<T>
where
T: Serialize + Send + Sync + Clone + 'static,
{
fn execute(&self, _: &[ChildRef]) -> Result<Vec<u8>> {
bincode::serde::encode_to_vec(&self.0, bincode::config::standard())
.map_err(|e| ParcodeError::Serialization(e.to_string()))
}
fn estimated_size(&self) -> usize {
std::mem::size_of::<T>()
}
}
#[derive(Clone)]
struct MapContainerJob {
num_shards: u32,
}
impl<'a> SerializationJob<'a> for MapContainerJob {
fn execute(&self, _children_refs: &[ChildRef]) -> Result<Vec<u8>> {
Ok(self.num_shards.to_le_bytes().to_vec())
}
fn estimated_size(&self) -> usize {
4
}
}
impl<K, V> ParcodeVisitor for HashMap<K, V>
where
K: Serialize + DeserializeOwned + Hash + Eq + Send + Sync + Clone + 'static,
V: Serialize + DeserializeOwned + Send + Sync + Clone + ParcodeVisitor + 'static,
{
fn visit<'a>(
&'a self,
graph: &mut TaskGraph<'a>,
parent_id: Option<ChunkId>,
config_override: Option<JobConfig>,
) {
let is_map_optimized = config_override.map(|c| c.is_map).unwrap_or(false);
let total_items = self.len();
if !is_map_optimized {
let job = if total_items != 0 {
self.create_job(config_override)
} else {
Box::new(MapContainerJob { num_shards: 0 })
};
let my_id = graph.add_node(job);
if let Some(p) = parent_id {
graph.link_parent_child(p, my_id);
}
return;
}
let target_items_per_bucket = 2000;
let num_shards = if total_items == 0 {
0
} else {
(total_items / target_items_per_bucket).clamp(1, 1024)
};
let mut buckets = vec![Vec::new(); num_shards];
for (k, v) in self {
let h = hash_key(k);
let idx = usize::try_from(h).unwrap_or(0) % num_shards;
if let Some(bucket) = buckets.get_mut(idx) {
bucket.push((k, v));
}
}
let container_inner = Box::new(MapContainerJob {
num_shards: u32::try_from(num_shards).unwrap_or(u32::MAX),
});
let container_job: Box<dyn SerializationJob<'a> + 'a> = if let Some(cfg) = config_override {
Box::new(crate::rt::ConfiguredJob::new(container_inner, cfg))
} else {
container_inner
};
let my_id = graph.add_node(container_job);
if let Some(p) = parent_id {
graph.link_parent_child(p, my_id);
}
for bucket in buckets {
let child_id = graph.add_node(Box::new(PlaceholderJob));
graph.link_parent_child(my_id, child_id);
#[allow(clippy::unnecessary_to_owned)]
for (_, v) in bucket.iter().cloned() {
v.visit_inlined(graph, child_id, None);
}
let shard_inner = Box::new(MapShardJob { items: bucket });
let shard_job: Box<dyn SerializationJob<'a> + 'a> = if let Some(cfg) = config_override {
Box::new(crate::rt::ConfiguredJob::new(shard_inner, cfg))
} else {
shard_inner
};
graph.replace_job(child_id, shard_job);
}
}
fn create_job<'a>(
&'a self,
config_override: Option<JobConfig>,
) -> Box<dyn SerializationJob<'a> + 'a> {
let base_job = Box::new(PrimitiveJob(self.clone()));
if let Some(cfg) = config_override {
Box::new(crate::rt::ConfiguredJob::new(base_job, cfg))
} else {
base_job
}
}
}
impl<T> ParcodeVisitor for &T
where
T: ParcodeVisitor,
{
fn visit<'a>(
&'a self,
graph: &mut TaskGraph<'a>,
parent_id: Option<ChunkId>,
config_override: Option<JobConfig>,
) {
(**self).visit(graph, parent_id, config_override);
}
fn create_job<'a>(
&'a self,
config_override: Option<JobConfig>,
) -> Box<dyn SerializationJob<'a> + 'a> {
(**self).create_job(config_override)
}
}
struct PlaceholderJob;
impl<'a> SerializationJob<'a> for PlaceholderJob {
fn execute(&self, _: &[ChildRef]) -> Result<Vec<u8>> {
Err(ParcodeError::Internal(
"PlaceholderJob executed! This is a bug.".into(),
))
}
fn estimated_size(&self) -> usize {
0
}
}
macro_rules! impl_primitive_visitor {
($($t:ty),*) => {
$(
impl ParcodeVisitor for $t {
fn visit<'a>(&'a self, graph: &mut TaskGraph<'a>, parent_id: Option<ChunkId>, config_override: Option<JobConfig>) {
let job = self.create_job(config_override);
let my_id = graph.add_node(job);
if let Some(pid) = parent_id {
graph.link_parent_child(pid, my_id);
}
}
fn create_job<'a>(&'a self, config_override: Option<JobConfig>) -> Box<dyn SerializationJob<'a> + 'a> {
let base_job = Box::new(PrimitiveJob(self.clone()));
if let Some(cfg) = config_override {
Box::new(crate::rt::ConfiguredJob::new(base_job, cfg))
} else {
base_job
}
}
}
)*
}
}
impl_primitive_visitor!(
u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64, bool, String
);