Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3debf33
Prototype diff UI, document prototype API
johallar May 18, 2026
49e7e98
POC timelines and real table comparisons
johallar May 19, 2026
d8c8125
Swappable, some UX improvements
johallar May 19, 2026
9fcc6eb
Don't close automatically annoyingly
johallar May 19, 2026
1abcb8f
Refining the timeline contract
johallar May 19, 2026
81e7cf1
Adding stat cards at the top
johallar May 19, 2026
ea17722
UI Fixups
johallar May 19, 2026
8f986c3
Color query names
johallar May 19, 2026
43622ea
Negative is good
johallar May 19, 2026
47f38ec
Remove match_kind, redundant
johallar May 20, 2026
ba3de5e
Upate md
johallar May 20, 2026
be0e378
Diff engines per query
johallar May 20, 2026
c9ccbab
Handle baseline + multiple competitor queries
johallar May 20, 2026
400016b
Handle N competitors anywhere
johallar May 20, 2026
92b273b
Adds legend, other stuff
johallar May 20, 2026
c35ccb1
Human updated contract
johallar May 20, 2026
859f0ff
Update app to fit the new API
johallar May 20, 2026
a3acf32
Operator diffs a little better
johallar May 20, 2026
fa2fef0
Competitor->Comparison, use the real API for operator table stats
johallar May 20, 2026
35c4b1e
One option for multi value table cells
johallar May 20, 2026
7debd3e
Selectable stat for bar spark charts, combine engines into one table
johallar May 20, 2026
f9daeb8
Configure grid a little different
johallar May 20, 2026
4668532
Pretty cool diff viz on the comparison timelines
johallar May 20, 2026
8e2c3fa
Genericize simple stacked bar
johallar May 21, 2026
4c7043c
Make tooltips useful
johallar May 21, 2026
57918ef
Fix diff table group by functionality
johallar May 21, 2026
9b2d133
Update default group by setup
johallar May 21, 2026
b6e5b26
Fix unit test
johallar May 21, 2026
0e1324d
Heatmap implementation, line up axes
johallar May 21, 2026
1399e3e
Line up timelines, spiff up
johallar May 21, 2026
24811f6
Red/Blue scale for color blind safety
johallar May 22, 2026
1c92b75
line chart experiments
johallar May 22, 2026
43aa030
Some other variations on color and line charts
johallar May 22, 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
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
- [Channel](./modeling/common/channel.md)
- [Domain-Specific Models](./domains/README.md)
- [Query Engines](./domains/query_engine/README.md)
- [Query Profile Diff API](./domains/query_engine/query_profile_diff.md)
- [Examples](./domains/query_engine/examples/README.md)
- [Simulator](./domains/query_engine/examples/simulator.md)
172 changes: 172 additions & 0 deletions docs/domains/query_engine/query_profile_diff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Query Profile Diff API

The query profile diff APIs compare two query profiles. The profiles may come
from different engines, so each side of the comparison carries its own engine
ID.
The UI also uses the profile diff contract as its internal diff view model when
it builds query diffs client-side from real `QueryBundle` API responses.
The diff UI presents that pairwise contract as one Baseline Query and one or
more Competitor Queries. Each competitor renders an independent pairwise diff
where `query_a` is the baseline and `query_b` is that competitor.

## Profile Diff Endpoint

```http
POST /api/query-profile-diff
```

### Request

```ts
export interface QueryProfileDiffQueryRef {
engine_id: string;
query_id: string;
}

export interface QueryProfileDiffRequest {
query_a: QueryProfileDiffQueryRef;
query_b: QueryProfileDiffQueryRef;
}
```

For a single pairwise comparison, Query A is the baseline and Query B is the
competitor. Numeric deltas are always `A - B`.

### Response

```ts
export type QueryProfileDiffScenario =
| "plans_equal"
| "plans_different"
| "plans_incomparable";

export interface QueryProfileDiffQuerySummary {
id: string;
engine_id: string;
engine_name: string | null;
instance_name: string | null;
query_group_id?: string | null;
query_group_name?: string | null;
}

export interface QueryProfileDiffOperatorRef {
id: string;
label: string;
operator_type_name: string | null;
plan_id: string | null;
}

export interface QueryProfileDiffStatDelta {
a: StatValue;
b: StatValue;
delta: number | null;
percent_delta: number | null;
}

export interface QueryProfileDiffOperatorDelta {
operator_a: QueryProfileDiffOperatorRef | null;
operator_b: QueryProfileDiffOperatorRef | null;
stats: Record<string, QueryProfileDiffStatDelta>;
}

export interface QueryProfileDiffPlanComparison {
matched_operator_count: number;
unmatched_operator_a_count: number;
unmatched_operator_b_count: number;
}

export interface QueryProfileDiffResponse {
scenario: QueryProfileDiffScenario;
query_a: QueryProfileDiffQuerySummary;
query_b: QueryProfileDiffQuerySummary;
plan_comparison: QueryProfileDiffPlanComparison;
operator_diffs: QueryProfileDiffOperatorDelta[];
warnings?: string[];
}
```

`StatValue` comes from the UI utility types and may be a string, number,
boolean, null, or string array. Numeric stats include `delta` and
`percent_delta`; non-numeric or missing values use `delta: null`.
`percent_delta` is `delta / b` when B is numeric and nonzero, otherwise null.

## Timeline Diff Endpoint

```http
POST /api/timeline/diff
```

The timeline diff endpoint accepts two or more single-timeline requests,
returns each requested timeline, and adds a derived delta timeline. Each
timeline entry names the engine that should execute that single-timeline
request.

### Request

```ts
export type QueryProfileDiffTimelineEntries<T> = [T, T, ...T[]];

export interface QueryProfileDiffTimelineEntry<T> {
engine_id: string;
timeline: T;
}

export interface QueryProfileDiffTimelineRequest {
timelines: QueryProfileDiffTimelineEntries<
QueryProfileDiffTimelineEntry<
SingleTimelineRequest<QueryFilter, TaskFilter>
>
>;
delta_config: TimelineConfig;
}
```

`timelines` must contain at least Query A and Query B entries. Additional
entries may be included when the caller needs the same request/response bundle
for overlays or comparison context. `delta_config` controls the output window
and binning for the derived delta timeline.

### Response

```ts
export interface QueryProfileDiffTimelineResponse {
timelines: QueryProfileDiffTimelineEntries<SingleTimelineResponse>;
delta: SingleTimelineResponse;
warnings?: string[];
}
```

The first response in `timelines` corresponds to Query A and the second
corresponds to Query B. The `delta` timeline is sampled into `delta_config`.
The current implementation represents the delta as binned positive magnitudes:
one series for where Query A is higher and one series for where Query B is
higher.

## V1 Semantics

- `plans_equal` means a structural match: topology plus ordered operator
type/name signatures match, ignoring run-specific IDs.
- `operator_diffs` contains matched operator pairs for equal plans.
- Numeric stats include `delta` and optional `percent_delta`; non-numeric or
missing values use `delta: null`.
- `plans_different` means operator-to-operator diffs are unavailable. The
response reports matched and unmatched counts and may include a warning.
- `plans_incomparable` is reserved for profiles that cannot be compared, such
as unsupported or missing plan data.
- Different-plan aggregate rows are a planned follow-up.

## Client Surface

The TypeScript client exposes:

```ts
fetchQueryProfileDiff(request)
fetchQueryProfileDiffTimeline(request)
useQueryProfileDiff(params, options?)
useQueryProfileDiffTimeline(params, options?)
```

The current UI can also build a `QueryProfileDiffResponse` locally from two
`QueryBundle` responses. That local path uses the same response contract so the
table, stats, and timeline views can consume either API-backed or client-built
diffs.
26 changes: 26 additions & 0 deletions ui/packages/@quent/client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import type {
EntityRef,
Engine,
} from '@quent/utils';
import type {
DiffRequest,
DiffResponse,
DiffTimelineRequest,
DiffTimelineResponse,
} from './queryProfileDiffTypes';

interface ApiFetchOptions {
params?: Record<string, string | number | boolean>;
Expand Down Expand Up @@ -104,3 +110,23 @@ export async function fetchBulkTimelines(
},
});
}

export async function fetchQueryProfileDiff(request: DiffRequest): Promise<DiffResponse> {
return apiFetch<DiffResponse>('/query-profile-diff', {
fetchOptions: {
method: 'POST',
body: JSON.stringify(request),
},
});
}

export async function fetchQueryProfileDiffTimeline(
request: DiffTimelineRequest
): Promise<DiffTimelineResponse> {
return apiFetch<DiffTimelineResponse>('/timeline/diff', {
fetchOptions: {
method: 'POST',
body: JSON.stringify(request),
},
});
}
27 changes: 27 additions & 0 deletions ui/packages/@quent/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export {
fetchListQueries,
fetchSingleTimeline,
fetchBulkTimelines,
fetchQueryProfileDiff,
fetchQueryProfileDiffTimeline,
} from './api';

// queryOptions factories
Expand All @@ -22,10 +24,35 @@ export { queryGroupsQueryOptions } from './queryGroups';
export { queriesQueryOptions } from './queries';
export { singleTimelineQueryOptions } from './timeline';
export { bulkTimelineQueryOptions } from './bulkTimelines';
export {
queryProfileDiffQueryOptions,
queryProfileDiffTimelineQueryOptions,
} from './queryProfileDiff';
export {
buildQueryProfileDiffFromBundles,
buildQueryProfileDiffResponseFromBundles,
} from './queryProfileDiffFromBundles';

// Hooks
export { useQueryBundle } from './queryBundle';
export { useEngines } from './engines';
export { useQueryGroups } from './queryGroups';
export { useQueries } from './queries';
export { useTimeline } from './timeline';
export { useQueryProfileDiff, useQueryProfileDiffTimeline } from './queryProfileDiff';

export type {
Compatibility,
DiffDelta,
DiffOperatorDelta,
DiffOperatorRef,
QueryDiff,
DiffQueryRef,
DiffQuerySummary,
DiffRequest,
DiffResponse,
DiffTimelineEntry,
DiffTimelineEntries,
DiffTimelineRequest,
DiffTimelineResponse,
} from './queryProfileDiff';
70 changes: 70 additions & 0 deletions ui/packages/@quent/client/src/queryProfileDiff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from 'vitest';
import {
queryProfileDiffQueryOptions,
queryProfileDiffTimelineQueryOptions,
} from './queryProfileDiff';
import type { DiffTimelineRequest } from './queryProfileDiffTypes';

describe('queryProfileDiffQueryOptions', () => {
it('builds a stable key from both engine and query ids', () => {
const request = {
baselineQuery: { engine_id: 'engine-a', query_id: 'query-a' },
comparisonQueries: [
{ engine_id: 'engine-b', query_id: 'query-b' },
{ engine_id: 'engine-c', query_id: 'query-c' },
],
};
const options = queryProfileDiffQueryOptions({
request,
});

expect(options.queryKey).toEqual(['queryProfileDiff', request]);
});

it('builds diff timeline options around the full request', () => {
const request: DiffTimelineRequest = {
timelines: [
{
engine_id: 'engine-a',
timeline: {
entry: {
ResourceGroup: {
resource_group_id: 'root-a',
resource_type_name: 'GPU',
long_entities_threshold_s: null,
entity_filter: { entity_type_name: null },
app_params: { operator_id: null },
config: { num_bins: 200, start: 0, end: 10 },
},
},
app_params: { query_id: 'query-a' },
},
},
{
engine_id: 'engine-b',
timeline: {
entry: {
ResourceGroup: {
resource_group_id: 'root-b',
resource_type_name: 'GPU',
long_entities_threshold_s: null,
entity_filter: { entity_type_name: null },
app_params: { operator_id: null },
config: { num_bins: 200, start: 0, end: 12 },
},
},
app_params: { query_id: 'query-b' },
},
},
],
delta_config: { num_bins: 200, start: 0, end: 12 },
};

const options = queryProfileDiffTimelineQueryOptions({ request });

expect(options.queryKey).toEqual(['queryProfileDiffTimeline', request]);
});
});
Loading