Skip to content

Commit f4eb034

Browse files
authored
perf(cubeorchestrator): Hand-written Deserialize for DBResponsePrimitive (-95%, ~20x parse) (#10853)
The derived `#[serde(untagged)]` impl funnels every value through `serde::__private::de::Content`, materializing an intermediate tree per element. For wire payloads dominated by strings/numbers/null (the common CubeStore row shape) that buffering is the bottleneck. A direct `Visitor` dispatches on the JSON token in one pass; only the rare seq/map branches pay the `Value`-materialization cost via `Uncommon`. Measured on `QueryResult::from_js_raw_data` bench (criterion, 100 samples for c16; --quick elsewhere): | Bench | Before | After | Difference | Speedup | Faster by | | ------------------------------ | ---------- | --------- | ----------- | ------- | --------- | | parse_only / c08_r10000 | 39.80 ms | 1.96 ms | -37.84 ms | ~20x | -95.1% | | parse_plus_build / c08_r10000 | 39.99 ms | 2.25 ms | -37.74 ms | ~18x | -94.4% | | parse_only / c16_r10000 | 82.39 ms | 4.03 ms | -78.36 ms | 20.4x | -95.1% | | parse_plus_build / c16_r10000 | 79.94 ms | 4.79 ms | -75.15 ms | 16.7x | -94.0% | | parse_only / c16_r100000 | 836.81 ms | 40.25 ms | -796.56 ms | 20.8x | -95.2% | | parse_plus_build / c16_r100000 | 832.44 ms | 55.58 ms | -776.86 ms | 15.0x | -93.3% | | parse_only / c32_r100000 | 1662.60 ms | 81.66 ms | -1580.94 ms | ~20x | -95.1% | | parse_plus_build / c32_r100000 | 1677.50 ms | 114.22 ms | -1563.28 ms | ~15x | -93.2% | Throughput on pure parse jumps from ~2 Melem/s to ~40 Melem/s. `parse_plus_build` gains less because the transpose into `QueryResult` now dominates once parsing is cheap.
1 parent c12e268 commit f4eb034

1 file changed

Lines changed: 92 additions & 2 deletions

File tree

rust/cube/cubeorchestrator/src/query_result_transform.rs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ use anyhow::{bail, Context, Result};
99
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
1010
use indexmap::IndexMap;
1111
use itertools::multizip;
12-
use serde::{Deserialize, Serialize};
12+
use serde::{
13+
de::{self, MapAccess, SeqAccess, Visitor},
14+
Deserialize, Deserializer, Serialize,
15+
};
1316
use serde_json::Value;
1417
use std::{
1518
collections::{HashMap, HashSet},
@@ -961,7 +964,7 @@ pub struct RequestResultArray {
961964
pub results: Vec<RequestResultData>,
962965
}
963966

964-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
967+
#[derive(Debug, Clone, Serialize, PartialEq)]
965968
#[serde(untagged)]
966969
pub enum DBResponsePrimitive {
967970
Null,
@@ -971,6 +974,93 @@ pub enum DBResponsePrimitive {
971974
Uncommon(Value),
972975
}
973976

977+
// Hand-written `Deserialize` that avoids serde's untagged-enum buffering.
978+
impl<'de> Deserialize<'de> for DBResponsePrimitive {
979+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
980+
where
981+
D: Deserializer<'de>,
982+
{
983+
struct DBResponsePrimitiveVisitor;
984+
985+
impl<'de> Visitor<'de> for DBResponsePrimitiveVisitor {
986+
type Value = DBResponsePrimitive;
987+
988+
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
989+
f.write_str("a JSON primitive (null, bool, number, string) or container")
990+
}
991+
992+
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
993+
Ok(DBResponsePrimitive::Boolean(v))
994+
}
995+
996+
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
997+
Ok(DBResponsePrimitive::Number(v as f64))
998+
}
999+
1000+
fn visit_i128<E: de::Error>(self, v: i128) -> Result<Self::Value, E> {
1001+
Ok(DBResponsePrimitive::Number(v as f64))
1002+
}
1003+
1004+
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
1005+
Ok(DBResponsePrimitive::Number(v as f64))
1006+
}
1007+
1008+
fn visit_u128<E: de::Error>(self, v: u128) -> Result<Self::Value, E> {
1009+
Ok(DBResponsePrimitive::Number(v as f64))
1010+
}
1011+
1012+
fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
1013+
Ok(DBResponsePrimitive::Number(v))
1014+
}
1015+
1016+
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
1017+
Ok(DBResponsePrimitive::String(v.to_owned()))
1018+
}
1019+
1020+
fn visit_borrowed_str<E: de::Error>(self, v: &'de str) -> Result<Self::Value, E> {
1021+
Ok(DBResponsePrimitive::String(v.to_owned()))
1022+
}
1023+
1024+
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
1025+
Ok(DBResponsePrimitive::String(v))
1026+
}
1027+
1028+
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
1029+
Ok(DBResponsePrimitive::Null)
1030+
}
1031+
1032+
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
1033+
Ok(DBResponsePrimitive::Null)
1034+
}
1035+
1036+
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1037+
where
1038+
D: Deserializer<'de>,
1039+
{
1040+
Deserialize::deserialize(deserializer)
1041+
}
1042+
1043+
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1044+
where
1045+
A: SeqAccess<'de>,
1046+
{
1047+
let value = Value::deserialize(de::value::SeqAccessDeserializer::new(seq))?;
1048+
Ok(DBResponsePrimitive::Uncommon(value))
1049+
}
1050+
1051+
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
1052+
where
1053+
A: MapAccess<'de>,
1054+
{
1055+
let value = Value::deserialize(de::value::MapAccessDeserializer::new(map))?;
1056+
Ok(DBResponsePrimitive::Uncommon(value))
1057+
}
1058+
}
1059+
1060+
deserializer.deserialize_any(DBResponsePrimitiveVisitor)
1061+
}
1062+
}
1063+
9741064
impl Display for DBResponsePrimitive {
9751065
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9761066
let str = match self {

0 commit comments

Comments
 (0)