Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2981414
feat(ui): add quantities to operator statistics
johanpel Jul 28, 2026
1c4a240
test(simulator): remove quantity mapping tests
johanpel Jul 28, 2026
36721d2
test(ui): update DAG statistic fixture
johanpel Jul 28, 2026
dc68e89
feat(simulator): add duration quantities
johanpel Jul 29, 2026
04ea807
add helper for quantity-aware stat formatting
cmatzenbach Jul 29, 2026
3d748c5
fix(simulator): rename scaled duration statistics
johanpel Jul 30, 2026
5e3f0e9
Merge branch 'stats-quantities' of github.com:johanpel/quent into sta…
cmatzenbach Jul 30, 2026
800c6c7
type fixes
cmatzenbach Jul 29, 2026
387989b
use quantity-aware formatting in DAGNodeInfoPanel
cmatzenbach Jul 29, 2026
c4c4904
use quantity-aware formatting for DAG node color labels and legend
cmatzenbach Jul 29, 2026
05da69f
use quantity-aware formatting in operator statistics pivot table
cmatzenbach Jul 29, 2026
577648a
fix(simulator): gate duration metadata on scaling
johanpel Jul 31, 2026
519c968
Remove inline imports
cmatzenbach Jul 31, 2026
5ac5df5
Set up hook and atoms to provide stat quantity specs
cmatzenbach Jul 31, 2026
f6b26ff
pass fmt function to ContinuousLegend instead of QuantitySpec
cmatzenbach Jul 31, 2026
21861f3
Merge branch 'stats-quantities' of github.com:johanpel/quent into sta…
cmatzenbach Jul 31, 2026
385de43
wire quantity specs through to operator statistics display, abandon c…
cmatzenbach Jul 31, 2026
780b5b7
Remove prop - no longer needed
cmatzenbach Jul 31, 2026
8e82c6d
Merge branch 'main' into stats-quantities
cmatzenbach Jul 31, 2026
5904591
linting
cmatzenbach Jul 31, 2026
84754fc
Fix type error, and ensure bigints go through bigint supported helper
cmatzenbach Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion domains/query_engine/analyzer/src/plain/legacy/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,15 @@ impl OperatorEntity for Operator {
custom_statistics: s
.custom_attributes
.iter()
.map(|DynamicAttribute { key, value }| (key.clone(), value.clone()))
.map(|DynamicAttribute { key, value }| {
(
key.clone(),
ui::OperatorStatistic {
value: value.clone(),
quantity: None,
},
)
})
.collect(),
});

Expand Down
15 changes: 12 additions & 3 deletions domains/query_engine/ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,19 @@ pub struct Plan {
pub edges: Vec<Edge>,
}

#[derive(TS, Debug, Serialize)]
pub struct OperatorStatistic {
/// The value of this statistic.
pub value: Option<DynamicValue>,
/// The key of the [`QuantitySpec`] in [`QueryBundle::quantity_specs`] used
/// to display this statistic.
pub quantity: Option<String>,
}

#[derive(TS, Debug, Serialize)]
pub struct OperatorStatistics {
/// Custom statistics
pub custom_statistics: HashMap<String, Option<DynamicValue>>,
/// Custom statistics.
pub custom_statistics: HashMap<String, OperatorStatistic>,
}

#[derive(TS, Debug, Serialize)]
Expand Down Expand Up @@ -298,7 +307,7 @@ pub struct QueryBundle<E> {
/// A list of unique operator type names.
pub unique_operator_names: Vec<String>,

/// Quantity specifications for capacity display, keyed by capacity name.
/// Quantity specifications for displaying values, keyed by quantity name.
pub quantity_specs: HashMap<String, QuantitySpec>,

/// The number of nanoseconds passed since the Unix epoch at which the
Expand Down
101 changes: 95 additions & 6 deletions examples/simulator/analyzer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use quent_dynamic_attributes::DynamicValue;
use quent_events::Event;
pub use quent_query_engine_analyzer::QueryEngineModel;
use quent_query_engine_analyzer::{
Expand Down Expand Up @@ -67,11 +68,77 @@ pub mod view;
const MEASURE_TASKS: &str = "tasks";
/// Data-flow measure summing memory bytes held in each (state, location) cell.
const MEASURE_BYTES: &str = "bytes";
const QUANTITY_BYTES: &str = "bytes";
const QUANTITY_SECONDS: &str = "seconds";
const BYTE_OPERATOR_STATISTICS: &[&str] = &[
"average_partition_size_bytes",
"avg_key_length_bytes",
"bloom_filter_size_bytes",
"build_side_bytes",
"bytes_read",
"bytes_written",
"hash_table_size_bytes",
"input_bytes",
"network_bytes_sent",
"output_bytes",
"peak_memory_bytes",
"per_file_bytes_read",
"probe_side_bytes",
"spill_bytes",
];
const SECOND_OPERATOR_STATISTICS: &[&str] = &[
"build_time_ns",
"cpu_time_ns",
"decompress_time_ns",
"flush_time_ns",
"hash_time_ns",
"io_wait_ns",
"merge_time_ns",
"network_time_ns",
"partition_time_ns",
"predicate_filter_time_ns",
"probe_time_ns",
"serialization_time_ns",
"wall_time_ns",
];
/// Data-flow dimension key for states that hold no memory resource.
const DIMENSION_NONE: &str = "none";
/// Type name of stdlib memory resources as recorded by the model.
const MEMORY_TYPE_NAME: &str = "memory";

fn operator_statistic_quantity(name: &str) -> Option<&'static str> {
BYTE_OPERATOR_STATISTICS
.contains(&name)
.then_some(QUANTITY_BYTES)
}

fn scale_operator_statistic(name: &str, value: &Option<DynamicValue>) -> Option<DynamicValue> {
if !SECOND_OPERATOR_STATISTICS.contains(&name) {
return None;
}
match value {
Some(DynamicValue::U64(nanoseconds)) => {
let seconds = *nanoseconds as f64 / 1_000_000_000.0;
Some(DynamicValue::F64(seconds))
}
_ => None,
}
}
Comment on lines +109 to +126

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File outline ==\n'
ast-grep outline examples/simulator/analyzer/src/lib.rs --view expanded || true

printf '\n== Relevant symbols/search ==\n'
rg -n "operator_statistic_quantity|scale_operator_statistic|quantity_specs|mod tests|#\\[test\\]" examples/simulator/analyzer/src/lib.rs examples/simulator/analyzer/src -S || true

printf '\n== File line counts ==\n'
wc -l examples/simulator/analyzer/src/lib.rs || true

printf '\n== Targeted source slices ==\n'
sed -n '1,220p' examples/simulator/analyzer/src/lib.rs

Repository: rapidsai/quent

Length of output: 10902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Search for tests in nearby files ==\n'
fd -a -t f 'test' examples/simulator/analyzer || true
fd -a -t f . examples/simulator/analyzer/src || true

printf '\n== Cargo manifest ==\n'
sed -n '1,220p' examples/simulator/analyzer/Cargo.toml || true

printf '\n== Search whole analyzer tree for helper names ==\n'
rg -n "operator_statistic_quantity|scale_operator_statistic|quantity_specs" examples/simulator/analyzer -S || true

Repository: rapidsai/quent

Length of output: 2127


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Focused test search in workspace ==\n'
rg -n "operator_statistic_quantity|scale_operator_statistic|quantity_specs|nanoseconds|QUANTITY_BYTES|QUANTITY_SECONDS" . -S --glob '!target' --glob '!dist' --glob '!build' || true

Repository: rapidsai/quent

Length of output: 3347


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Tail of lib.rs ==\n'
sed -n '1180,1213p' examples/simulator/analyzer/src/lib.rs

printf '\n== Any test modules in lib.rs ==\n'
rg -n "mod tests|#\\[cfg\\(test\\)\\]|#\\[test\\]" examples/simulator/analyzer/src/lib.rs -S || true

Repository: rapidsai/quent

Length of output: 1412


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Tail of lib.rs ==\n'
sed -n '1180,1213p' examples/simulator/analyzer/src/lib.rs

printf '\n== Test markers in lib.rs ==\n'
rg -n "mod tests|#\\[cfg\\(test\\)\\]|#\\[test\\]" examples/simulator/analyzer/src/lib.rs -S || true

Repository: rapidsai/quent

Length of output: 1408


Add tests for the new quantity helpers. Cover byte/second names, unknown names, nanosecond-to-second scaling, unchanged None/non-U64 values, and the quantity_specs() keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/simulator/analyzer/src/lib.rs` around lines 108 - 126, Add focused
tests for operator_statistic_quantity, scale_operator_statistic, and
quantity_specs(). Verify byte and second statistic names, unknown names,
nanosecond-to-second conversion, unchanged None and non-U64 values, and that
quantity_specs() exposes the expected keys.

Source: Coding guidelines


fn scaled_operator_statistic_name(name: String) -> String {
name.strip_suffix("_ns").unwrap_or(&name).to_owned()
}

fn quantity_specs() -> StdHashMap<String, QuantitySpec> {
[
("capacity_bytes".into(), QuantitySpec::bytes()),
(QUANTITY_BYTES.into(), QuantitySpec::bytes()),
(QUANTITY_SECONDS.into(), QuantitySpec::seconds()),
("unit".into(), QuantitySpec::unit()),
]
.into()
}

pub struct SimulatorUiAnalyzer {
pub model: SimulatorModel,
}
Expand Down Expand Up @@ -205,7 +272,33 @@ impl UiAnalyzer for SimulatorUiAnalyzer {
let query = query.to_ui()?;
let workers = view.workers().map(|w| (w.id(), w.to_ui(epoch))).collect();
let plans = view.plans().map(|p| (p.id(), p.to_ui())).collect();
let operators = view.operators().map(|o| (o.id(), o.to_ui(epoch))).collect();
let operators = view
.operators()
.map(|operator| {
let mut ui_operator = operator.to_ui(epoch);
if let Some(statistics) = &mut ui_operator.statistics {
statistics.custom_statistics =
std::mem::take(&mut statistics.custom_statistics)
.into_iter()
.map(|(name, mut statistic)| {
let name = if let Some(value) =
scale_operator_statistic(&name, &statistic.value)
{
statistic.value = Some(value);
statistic.quantity = Some(QUANTITY_SECONDS.to_owned());
scaled_operator_statistic_name(name)
} else {
statistic.quantity =
operator_statistic_quantity(&name).map(str::to_owned);
name
};
(name, statistic)
})
.collect();
}
(operator.id(), ui_operator)
})
.collect();
let ports = view.ports().map(|p| (p.id(), p.to_ui(epoch))).collect();
let unique_operator_names = view
.operators()
Expand Down Expand Up @@ -278,11 +371,7 @@ impl UiAnalyzer for SimulatorUiAnalyzer {
plan_tree,
resource_tree,
unique_operator_names,
quantity_specs: [
("capacity_bytes".into(), QuantitySpec::bytes()),
("unit".into(), QuantitySpec::unit()),
]
.into(),
quantity_specs: quantity_specs(),
start_time_unix_ns,
duration_s,
})
Expand Down
9 changes: 7 additions & 2 deletions ui/packages/@quent/components/src/dag/DAGChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ import type { DAGData } from '../services/query-plan/types';
import { QueryPlanNode, type QueryPlanNodeData } from '../query-plan/QueryPlanNode';
import { DAGLegend } from './DAGLegend';
import { parseCustomStatistics } from '../lib/queryBundle.utils';
import { continuousColor, getOperationTypeColor, buildOperatorColorMap } from '@quent/utils';
import { inferFieldFormatter } from '@quent/utils';
import {
continuousColor,
getOperationTypeColor,
buildOperatorColorMap,
inferFieldFormatter,
} from '@quent/utils';

// Edge geometry constants
const EDGE_STROKE_WIDTH_DEFAULT = 1.5;
Expand Down Expand Up @@ -338,6 +342,7 @@ const FlowLayout = ({
isDark,
baseColor: operatorColorMap.get(node.type.toLowerCase()),
flowBarVisible,
quantitySpecs: data.quantitySpecs,
},
style: {
width: NODE_LAYOUT_WIDTH,
Expand Down
20 changes: 16 additions & 4 deletions ui/packages/@quent/components/src/dag/DAGNodeInfoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ import {
} from '@quent/hooks';
import { DataText } from '../ui/data-text';
import { thinScrollbarClass } from '../ui/thin-scroll';
import { inferFieldFormatter, isNumericValue } from '@quent/utils';
import { formatStatWithQuantity, type QuantitySpec } from '@quent/utils';
import { DataFlowMatrix } from './DataFlowMatrix';

export const DAGNodeInfoPanel = ({ isDark = false }: { isDark?: boolean }) => {
export const DAGNodeInfoPanel = ({
isDark = false,
quantitySpecs,
}: {
isDark?: boolean;
quantitySpecs?: { [key: string]: QuantitySpec | undefined };
}) => {
const selectedNodeData = useSelectedNodeData();
const dataFlowEnabled = useDataFlowEnabled();
const dataFlowMeta = useDataFlowMeta();
Expand Down Expand Up @@ -78,7 +84,7 @@ export const DAGNodeInfoPanel = ({ isDark = false }: { isDark?: boolean }) => {
{selectedNodeData.nodeId}
</DataText>
</div>
{selectedNodeData.statistics?.map(({ key, value }) => (
{selectedNodeData.statistics?.map(({ key, value, quantity }) => (
<div key={key} className="text-xs">
{Array.isArray(value) ? (
<div className="flex items-center justify-between gap-0.5">
Expand All @@ -95,7 +101,13 @@ export const DAGNodeInfoPanel = ({ isDark = false }: { isDark?: boolean }) => {
<div className="flex items-center justify-between">
<DataText className="capitalize">{key.replace(/_/g, ' ')}:</DataText>
<DataText className="text-muted-foreground ml-1">
{isNumericValue(value) ? inferFieldFormatter(key)(value) : String(value)}
{typeof value === 'number'
? formatStatWithQuantity(
value,
key,
quantity && quantitySpecs ? quantitySpecs[quantity] : undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

golf: quantity ? quantitySpecs?.[quantity] : undefined

)
: String(value)}
</DataText>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,17 @@ function makeTagged(variant: string, value: unknown) {

function makeOperator(custom_statistics: Record<string, unknown> | undefined) {
return {
statistics: custom_statistics !== undefined ? { custom_statistics } : undefined,
statistics:
custom_statistics !== undefined
? {
custom_statistics: Object.fromEntries(
Object.entries(custom_statistics).map(([key, value]) => [
key,
{ value, quantity: null },
])
),
}
: undefined,
};
}

Expand All @@ -90,6 +100,17 @@ describe('parseCustomStatistics', () => {
expect(result).toEqual([{ key: 'rows', value: 42 }]);
});

it('preserves a quantity key', () => {
const op = {
statistics: {
custom_statistics: {
bytes: { value: makeTagged('UInt64', 1024), quantity: 'bytes' },
},
},
};
expect(parseCustomStatistics(op)).toEqual([{ key: 'bytes', value: 1024, quantity: 'bytes' }]);
});

it('unwraps a string tagged value', () => {
const op = makeOperator({ label: makeTagged('String', 'hello') });
expect(parseCustomStatistics(op)).toEqual([{ key: 'label', value: 'hello' }]);
Expand Down
18 changes: 11 additions & 7 deletions ui/packages/@quent/components/src/lib/queryBundle.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,20 @@ export function entityRefToEntitiesKey(entityRef: EntityRefKey): keyof QueryEnti
return ENTITY_REF_TO_ENTITIES_KEY[entityRef];
}

export function parseCustomStatistics(rawNode: unknown): Array<{ key: string; value: StatValue }> {
export function parseCustomStatistics(
rawNode: unknown
): Array<{ key: string; value: StatValue; quantity?: string }> {
const statistics = (rawNode as Operator)?.statistics?.custom_statistics;
if (!statistics) return [];

return Object.entries(statistics).map(([key, tagged]) => ({
key,
value: tagged
? unwrapTaggedValue(Object.values(tagged as unknown as Record<string, unknown>)[0])
: null,
}));
return Object.entries(statistics).map(([key, statistic]) => {
const { value, quantity } = statistic;
return {
key,
value: value ? unwrapTaggedValue(value) : null,
...(quantity !== null ? { quantity } : {}),
};
});
}

export function parsePortStatistics(rawPort: unknown): Array<{ key: string; value: StatValue }> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export type OperatorActiveSpanEntry = {
/** Plan ID this operator belongs to. */
planId: string;
/** Pre-computed custom statistics for the operator popup. */
statistics: Array<{ key: string; value: StatValue }>;
statistics: Array<{ key: string; value: StatValue; quantity?: string }>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ function DataCell({ row, stat }: DataCellProps<PivotedRow>) {
onMouseEnter: () => interaction.setHoveredStat(derived.buildHoveredStatInfo(stat)),
onMouseLeave: () => interaction.setHoveredStat(null),
};
const fmt = display.formatNumericValue;
if (!display.isAggregating) {
const val = row.values.get(stat) ?? null;
return (
Expand All @@ -189,7 +190,7 @@ function DataCell({ row, stat }: DataCellProps<PivotedRow>) {
style={{ backgroundColor: bg, boxShadow: cellHighlight }}
{...statCellProps}
>
{formatStatValue(val, stat)}
{typeof val === 'number' && fmt ? fmt(val, stat) : formatStatValue(val, stat)}
</td>
);
}
Expand All @@ -212,7 +213,9 @@ function DataCell({ row, stat }: DataCellProps<PivotedRow>) {
style={{ backgroundColor: bg, boxShadow: cellHighlight }}
{...statCellProps}
>
{formatNumericStat(displayVal, stat)}
{fmt && typeof displayVal === 'number'
? fmt(displayVal, stat)
: formatNumericStat(displayVal, stat)}
</td>
);
}
Expand Down Expand Up @@ -247,6 +250,8 @@ interface PivotedStatTableProps<TRow> {
/** Optional controlled sort state, forwarded to the underlying GroupedDataTable. */
sorting?: SortingState;
onSortingChange?: OnChangeFn<SortingState>;
/** Optional formatter for numeric stat values; falls back to inferFieldFormatter when absent. */
formatNumericValue?: (value: number, statName: string) => string;
}

export function PivotedStatTable<TRow>({
Expand All @@ -267,6 +272,7 @@ export function PivotedStatTable<TRow>({
onReorderStat,
sorting,
onSortingChange,
formatNumericValue,
}: PivotedStatTableProps<TRow>) {
const [nodePalette] = useNodeColorPalette();
const rowRefs = useRef<Map<string, HTMLTableRowElement>>(new Map());
Expand Down Expand Up @@ -518,8 +524,9 @@ export function PivotedStatTable<TRow>({
aggMode,
colorPalette: nodePalette,
darkMode: isDark,
formatNumericValue,
}),
[isAggregating, aggMode, nodePalette, isDark]
[isAggregating, aggMode, nodePalette, isDark, formatNumericValue]
);
const dndContextValue = useMemo(
() => ({
Expand Down
2 changes: 2 additions & 0 deletions ui/packages/@quent/components/src/pivot-table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export interface PivotTableDisplayConfig {
aggMode: AggMode;
colorPalette: ContinuousPaletteName;
darkMode: boolean;
/** Optional formatter for numeric stat values; falls back to inferFieldFormatter when absent. */
formatNumericValue?: (value: number, statName: string) => string;
}

// --- PivotedStatTable types ---
Expand Down
Loading