Skip to content

Commit c37936b

Browse files
committed
perf: batch staging arrow writes
Batch disk staging writes per output filename before writing to Arrow IPC. This reduces per-request writer mutex hold time and cuts the number of DiskWriter::write calls during high-volume OTEL metrics ingest. Also keep targeted hotpath probes around ingest, JSON conversion, staging, and parquet conversion paths, and skip object-store sync work for streams without local parquet/schema files.
1 parent bd563dc commit c37936b

12 files changed

Lines changed: 152 additions & 37 deletions

File tree

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ tokio = { version = "^1.43", default-features = false, features = [
9090
tokio-stream = { version = "0.1.17", features = ["fs"] }
9191
tokio-util = { version = "0.7" }
9292

93+
# perf
94+
hotpath = { version = "0.16.0", features = [
95+
"hotpath",
96+
"hotpath-cpu",
97+
"hotpath-alloc",
98+
"tokio"
99+
] }
100+
93101
# # Logging and Metrics
94102
# opentelemetry-proto = { version = "0.30.0", features = [
95103
# "gen-tonic",

src/event/format/json.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl EventFormat for Event {
6060

6161
// convert the incoming json to a vector of json values
6262
// also extract the arrow schema, tags and metadata from the incoming json
63+
#[hotpath::measure]
6364
fn to_data(
6465
self,
6566
schema: &HashMap<String, Arc<Field>>,
@@ -156,7 +157,7 @@ impl EventFormat for Event {
156157
infer_schema.clone(),
157158
]).map_err(|err| anyhow!("Could not merge schema of this event with that of the existing stream. {:?}", err))?;
158159
is_first = true;
159-
let schema = infer_schema
160+
let schema: Vec<Arc<Field>> = infer_schema
160161
.fields
161162
.iter()
162163
.filter(|field| !field.data_type().is_null())
@@ -327,6 +328,10 @@ fn rename_json_keys(values: Vec<Value>) -> Result<Vec<Value>, anyhow::Error> {
327328
.into_iter()
328329
.map(|value| {
329330
if let Value::Object(map) = value {
331+
if !map.keys().any(|key| key.starts_with('@')) {
332+
return Ok(Value::Object(map));
333+
}
334+
330335
// Collect original keys to check for collisions
331336
let original_keys: HashSet<String> = map.keys().cloned().collect();
332337

src/event/format/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ pub trait EventFormat: Sized {
162162
/// Returns the UTC time at ingestion
163163
fn get_p_timestamp(&self) -> DateTime<Utc>;
164164

165+
#[hotpath::measure]
165166
fn into_recordbatch(
166167
self,
167168
storage_schema: &HashMap<String, Arc<Field>>,
@@ -606,6 +607,28 @@ pub fn rename_per_record_type_mismatches(
606607
let Value::Object(map) = value else {
607608
return value;
608609
};
610+
let needs_rename = map.iter().any(|(key, val)| {
611+
if val.is_null() {
612+
return false;
613+
}
614+
let target_type = existing_schema
615+
.get(key)
616+
.map(|f| f.data_type())
617+
.or_else(|| inferred_types.get(key.as_str()).copied());
618+
let Some(target_type) = target_type else {
619+
return false;
620+
};
621+
if (val.is_array() || val.is_object())
622+
&& (target_type.is_list()
623+
|| matches!(target_type, DataType::Struct(_) | DataType::Map(_, _)))
624+
{
625+
return false;
626+
}
627+
!value_compatible_with_type(val, target_type, schema_version)
628+
});
629+
if !needs_rename {
630+
return Value::Object(map);
631+
}
609632
let new_map: serde_json::Map<String, Value> = map
610633
.into_iter()
611634
.map(|(key, val)| {

src/event/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ impl Event {
7272
is_first_event = self.is_first_event
7373
)
7474
)]
75+
#[hotpath::measure]
7576
pub fn process(self) -> Result<(), EventError> {
7677
let mut key = get_schema_key(&self.rb.schema().fields);
7778
if self.time_partition.is_some() {

src/handlers/http/modal/utils/ingest_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ pub async fn flatten_and_push_logs(
156156
skip(json, log_source, p_custom_fields, time_partition, telemetry_type, tenant_id),
157157
fields(stream_name, record_count = tracing::field::Empty)
158158
)]
159+
#[hotpath::measure]
159160
pub fn push_logs(
160161
stream_name: &str,
161162
json: Value,

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ use once_cell::sync::Lazy;
6666
pub use openid;
6767
use parseable::PARSEABLE;
6868
use reqwest::{Client, ClientBuilder};
69+
pub use {hotpath, tracing_actix_web, tracing_opentelemetry, tracing_subscriber};
6970
pub use {opentelemetry, opentelemetry_otlp, opentelemetry_proto, opentelemetry_sdk};
70-
pub use {tracing_actix_web, tracing_opentelemetry, tracing_subscriber};
7171

7272
// It is very unlikely that panic will occur when dealing with locks.
7373
pub const LOCK_EXPECT: &str = "Thread shouldn't panic while holding a lock";

src/otel/metrics.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,7 @@ fn process_resource_metrics<T, S, M>(
640640

641641
/// this function performs the custom flattening of the otel metrics
642642
/// and returns a `Vec` of `Value::Object` of the flattened json
643+
#[hotpath::measure]
643644
pub fn flatten_otel_metrics(message: MetricsData, tenant_id: &str) -> Vec<Value> {
644645
process_resource_metrics(
645646
&message.resource_metrics,

src/parseable/staging/writer.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
use std::{
2121
collections::{HashMap, HashSet},
22-
fs::{File, OpenOptions},
22+
fs::{self, File, OpenOptions},
2323
io::BufWriter,
2424
path::PathBuf,
2525
sync::Arc,
@@ -41,10 +41,83 @@ use crate::{
4141

4242
use super::StagingError;
4343

44+
const DISK_WRITE_BATCH_ROWS: usize = 16_384;
45+
4446
#[derive(Default)]
4547
pub struct Writer {
4648
pub mem: MemWriter<16384>,
4749
pub disk: HashMap<String, DiskWriter>,
50+
disk_pending: HashMap<String, PendingDiskBatch>,
51+
}
52+
53+
impl Writer {
54+
#[hotpath::measure]
55+
pub fn push_disk(
56+
&mut self,
57+
filename: String,
58+
rb: &RecordBatch,
59+
file_path: PathBuf,
60+
range: TimeRange,
61+
) -> Result<(), StagingError> {
62+
let pending = self.disk_pending.entry(filename.clone()).or_default();
63+
pending.rows += rb.num_rows();
64+
pending.range.get_or_insert(range);
65+
pending.batches.push(rb.clone());
66+
67+
if pending.rows >= DISK_WRITE_BATCH_ROWS {
68+
self.flush_pending_disk(&filename, file_path)?;
69+
}
70+
71+
Ok(())
72+
}
73+
74+
pub fn flush_pending_disk(
75+
&mut self,
76+
filename: &str,
77+
file_path: PathBuf,
78+
) -> Result<(), StagingError> {
79+
let Some(pending) = self.disk_pending.remove(filename) else {
80+
return Ok(());
81+
};
82+
if pending.batches.is_empty() {
83+
return Ok(());
84+
}
85+
86+
let schema = pending.batches[0].schema();
87+
let batch = concat_batches(&schema, pending.batches.iter())?;
88+
match self.disk.get_mut(filename) {
89+
Some(writer) => writer.write(&batch)?,
90+
None => {
91+
let range = pending.range.expect("pending disk batch must have range");
92+
if let Some(parent) = file_path.parent() {
93+
fs::create_dir_all(parent)?;
94+
}
95+
let mut writer = DiskWriter::try_new(file_path, &schema, range)?;
96+
writer.write(&batch)?;
97+
self.disk.insert(filename.to_owned(), writer);
98+
}
99+
}
100+
101+
Ok(())
102+
}
103+
104+
pub fn flush_all_pending_disk(
105+
&mut self,
106+
data_path: &std::path::Path,
107+
) -> Result<(), StagingError> {
108+
let filenames = self.disk_pending.keys().cloned().collect_vec();
109+
for filename in filenames {
110+
self.flush_pending_disk(&filename, data_path.join(&filename))?;
111+
}
112+
Ok(())
113+
}
114+
}
115+
116+
#[derive(Default)]
117+
struct PendingDiskBatch {
118+
rows: usize,
119+
batches: Vec<RecordBatch>,
120+
range: Option<TimeRange>,
48121
}
49122

50123
pub struct DiskWriter {
@@ -77,6 +150,7 @@ impl DiskWriter {
77150
}
78151

79152
/// Write a single recordbatch into file
153+
#[hotpath::measure]
80154
pub fn write(&mut self, rb: &RecordBatch) -> Result<(), StagingError> {
81155
self.inner.write(rb).map_err(StagingError::Arrow)
82156
}

0 commit comments

Comments
 (0)