This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
The following commit(s) were added to refs/heads/main by this push:
new da087d7 bff dashboard: chunk widget MQE batches per booster-ui's
6-per-trip rule
da087d7 is described below
commit da087d7a90e69274948c4fdb13a318f9e2b80f6a
Author: Wu Sheng <[email protected]>
AuthorDate: Sun May 17 20:01:06 2026 +0800
bff dashboard: chunk widget MQE batches per booster-ui's 6-per-trip rule
The OAP GraphQL server has per-request complexity / depth limits.
Sending every widget × expression in ONE aliased query is fine for
dashboards with a handful of widgets, but the bigger templates
(istio mesh, k8s, postgresql) can push 10-15 widgets × multiple
expressions each into the same trip — past OAP's threshold, the
whole batch 5xx's and every cell shows "mqe batch failed" instead
of degrading per chunk.
Booster-ui (`src/hooks/useExpressionsProcessor.ts:271`) pins this
at `DashboardMaxQueryWidgets = 6` and fires chunks in parallel via
Promise.all. Same pattern here:
- Chunk widgets into groups of 6.
- Each chunk gets its own aliased `query DashboardMqe { ... }`
GraphQL trip.
- Promise.all dispatches them concurrently; wall-clock stays
close to a single round-trip (parallel I/O), but each chunk
stays inside the OAP per-query budget.
- aliasMap + result merging unchanged — the per-widget reducer
in Step 3 looks up `w{wIdx}_e{eIdx}` keys regardless of which
chunk they came from.
If any one chunk errors we still bail with `mqe batch failed`
across all widgets (matches the prior all-or-nothing behaviour);
finer-grained partial-failure handling can come later if needed.
---
apps/bff/src/http/query/dashboard.ts | 88 ++++++++++++++++++++++--------------
1 file changed, 54 insertions(+), 34 deletions(-)
diff --git a/apps/bff/src/http/query/dashboard.ts
b/apps/bff/src/http/query/dashboard.ts
index 92c518b..2752e9d 100644
--- a/apps/bff/src/http/query/dashboard.ts
+++ b/apps/bff/src/http/query/dashboard.ts
@@ -465,45 +465,65 @@ export function registerDashboardQueryRoute(app:
FastifyInstance, deps: Dashboar
}
}
- // Step 2 — batch all widget × expression queries into one GraphQL trip.
- const fragments: string[] = [];
+ // Step 2 — batch widget × expression queries via aliased
+ // `execExpression(...)` fragments. Mirrors booster-ui's
+ // `useExpressionsProcessor.fetchMetrics`: chunk widgets into
+ // groups of 6 and fire each chunk as a separate GraphQL trip
+ // in parallel. The OAP GraphQL server has per-request complexity
+ // / depth limits (booster pins it at 6 widgets per trip) and
+ // huge dashboards (10+ widgets × multiple expressions each)
+ // would otherwise blow past the threshold and 5xx the whole
+ // batch — losing every cell instead of degrading per chunk.
+ // Chunked + Promise.all keeps the wall-clock close to a single
+ // round-trip while staying inside OAP's per-query budget.
+ const MAX_WIDGETS_PER_BATCH = 6;
const aliasMap = new Map<string, { wIdx: number; eIdx: number }>();
- // Per-widget scope plumbing: an instance-scoped page passes
- // `serviceInstance` in the body; an endpoint page passes
- // `endpoint`. Widgets that opt into `layerScope` always win over
- // both (they ignore the selected entity by design — they're
- // layer-wide rollups). When neither override applies, we keep
- // the legacy Service-scope behavior.
const scopeHonorsInstance = scope === 'instance';
const scopeHonorsEndpoint = scope === 'endpoint';
- widgets.forEach((widget, wIdx) => {
- widget.expressions.forEach((expr, eIdx) => {
- const alias = `w${wIdx}_e${eIdx}`;
- aliasMap.set(alias, { wIdx, eIdx });
- fragments.push(
- buildFragment(alias, expr, serviceName, normal, window, {
- layerScope: widget.layerScope === true,
- serviceInstanceName:
- widget.layerScope !== true && scopeHonorsInstance ?
selectedInstance : null,
- endpointName:
- widget.layerScope !== true && scopeHonorsEndpoint ?
selectedEndpoint : null,
- }),
- );
- });
- });
+ const widgetChunks: { widget: DashboardWidget; wIdx: number }[][] = [];
+ for (let i = 0; i < widgets.length; i += MAX_WIDGETS_PER_BATCH) {
+ widgetChunks.push(
+ widgets.slice(i, i + MAX_WIDGETS_PER_BATCH).map((widget, idxInChunk)
=> ({
+ widget,
+ wIdx: i + idxInChunk,
+ })),
+ );
+ }
let data: Record<string, MqeResultShape> = {};
- if (fragments.length > 0) {
- const query = `query DashboardMqe { ${fragments.join('\n ')} }`;
- try {
- data = await graphqlPost<Record<string, MqeResultShape>>(opts,
query);
- } catch (err) {
- return reply.send({
- ...baseResp,
- reachable: false,
- error: err instanceof Error ? err.message : String(err),
- widgets: widgets.map((w) => ({ id: w.id, error: 'mqe batch failed'
})),
- });
+ try {
+ const chunkResults = await Promise.all(
+ widgetChunks.map(async (chunk) => {
+ const fragments: string[] = [];
+ for (const { widget, wIdx } of chunk) {
+ widget.expressions.forEach((expr, eIdx) => {
+ const alias = `w${wIdx}_e${eIdx}`;
+ aliasMap.set(alias, { wIdx, eIdx });
+ fragments.push(
+ buildFragment(alias, expr, serviceName, normal, window, {
+ layerScope: widget.layerScope === true,
+ serviceInstanceName:
+ widget.layerScope !== true && scopeHonorsInstance ?
selectedInstance : null,
+ endpointName:
+ widget.layerScope !== true && scopeHonorsEndpoint ?
selectedEndpoint : null,
+ }),
+ );
+ });
+ }
+ if (fragments.length === 0) return {} as Record<string,
MqeResultShape>;
+ const query = `query DashboardMqe { ${fragments.join('\n ')} }`;
+ return graphqlPost<Record<string, MqeResultShape>>(opts, query);
+ }),
+ );
+ for (const chunk of chunkResults) {
+ Object.assign(data, chunk);
}
+ } catch (err) {
+ return reply.send({
+ ...baseResp,
+ reachable: false,
+ error: err instanceof Error ? err.message : String(err),
+ widgets: widgets.map((w) => ({ id: w.id, error: 'mqe batch failed'
})),
+ });
}
// Step 3 — collapse per widget. Per-type handling: