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:

Reply via email to