sparkforensics-server 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/public/assets/{AutoscalingChurn-BOiUUfuY.js → AutoscalingChurn-lxZHWIwq.js} +1 -1
- package/public/assets/CacheUtilization-DuAJ-Lve.js +1 -0
- package/public/assets/CachingOpportunity-DC1t7o2_.js +1 -0
- package/public/assets/{ColdStart-ZHxPrJmI.js → ColdStart-BOYXeiUZ.js} +1 -1
- package/public/assets/{ConfigAudit-BkVnq2XE.js → ConfigAudit-MCPrJomC.js} +1 -1
- package/public/assets/{CoreUsageArea-DobrSXk2.js → CoreUsageArea-ChNuqGA0.js} +1 -1
- package/public/assets/Dashboard-VuyUGvZx.js +1 -0
- package/public/assets/DuplicatePlanSubtree-JZut9xwq.js +1 -0
- package/public/assets/{DurationHistogram-DlQv4ZCB.js → DurationHistogram-_7C1w0l4.js} +1 -1
- package/public/assets/{EmptyState-DqnSxTjJ.js → EmptyState-D_ulfIIi.js} +1 -1
- package/public/assets/{ExecutorUtilization-CuQUxaCd.js → ExecutorUtilization-B08E76jT.js} +1 -1
- package/public/assets/GcPressure-BZMi2fTx.js +1 -0
- package/public/assets/{ImpactEstimate-Df_laG-n.js → ImpactEstimate-36WxuPdV.js} +1 -1
- package/public/assets/{IncompleteRun-CpIkS4XZ.js → IncompleteRun-t8JU7HRt.js} +1 -1
- package/public/assets/{JobFailures-DKbvYc4J.js → JobFailures-CguB1bec.js} +1 -1
- package/public/assets/{MemoryUtilization-C5kHmXIb.js → MemoryUtilization-DDk8lz9-.js} +1 -1
- package/public/assets/OverBroadcast-BHXLt6s7.js +1 -0
- package/public/assets/PartitionSizing-COj3uwV-.js +1 -0
- package/public/assets/{PlanExplorer-DjGTZifP.js → PlanExplorer-CnCdEY_i.js} +1 -1
- package/public/assets/PlanGraphRoute-BesZOW1n.js +1 -0
- package/public/assets/{RetryWaste-C0znHd_w.js → RetryWaste-CcU4ctAn.js} +1 -1
- package/public/assets/{RowPagination-2icPidMP.js → RowPagination-D2aIcfGv.js} +1 -1
- package/public/assets/{ShuffleIO-L_XSnJ_V.js → ShuffleIO-NPHkWbg1.js} +1 -1
- package/public/assets/{Skew-w4uV5H_g.js → Skew-CzztP-C4.js} +1 -1
- package/public/assets/SlowHost-Dh2KHypN.js +1 -0
- package/public/assets/SmallFiles-CgYSuZQw.js +1 -0
- package/public/assets/{SpeculationWaste-xCKwjz2K.js → SpeculationWaste-C8J5z1WS.js} +1 -1
- package/public/assets/Spill-DwTQPZ_L.js +1 -0
- package/public/assets/{StageFailed-De3vVDbc.js → StageFailed-aFeIEh3p.js} +1 -1
- package/public/assets/{StageFindingGroup-R4OW-g4U.js → StageFindingGroup-DQmAE3uk.js} +1 -1
- package/public/assets/{StageHeader-CGeWqC-0.js → StageHeader-D6GBbcZp.js} +1 -1
- package/public/assets/{StagePill-BwwKLcFl.js → StagePill-CSKyM8Kd.js} +1 -1
- package/public/assets/{StageShape-D4LmPVyV.js → StageShape-J6U4cUcM.js} +1 -1
- package/public/assets/{StageSlowness-8-z6qn4Q.js → StageSlowness-C5F2v_ZO.js} +1 -1
- package/public/assets/Straggler-DcP3dFGA.js +1 -0
- package/public/assets/TaskFailures-C-vIEheo.js +1 -0
- package/public/assets/{TinyTask-DYXks5sT.js → TinyTask-Be-pvTjJ.js} +1 -1
- package/public/assets/UnderBroadcast-DfjYGhoK.js +1 -0
- package/public/assets/{finding-anchor-CDbHtqKT.js → finding-anchor-vVODA_VC.js} +1 -1
- package/public/assets/{inbox-M_EuWNiK.js → inbox-LtbS_kBS.js} +1 -1
- package/public/assets/{index-pnImGbgQ.js → index-27MbqTFr.js} +5 -3
- package/public/assets/index-CWjjbtyo.css +2 -0
- package/public/assets/parser-worker-jAP0kWvS.js +122 -0
- package/public/assets/{select-C-MZGNpb.js → select-CC2Lyrmi.js} +1 -1
- package/public/assets/{toggle-group-C3nz7UQ_.js → toggle-group-lmgO21t9.js} +1 -1
- package/public/assets/useRecentFiles-qH5Ap4xm.js +12 -0
- package/public/assets/{useSortMode-DHlKZeWv.js → useSortMode-CvPY99UV.js} +1 -1
- package/public/docs/404.html +2 -2
- package/public/docs/assets/{app.DsFQcynM.js → app.C5E-wy6e.js} +1 -1
- package/public/docs/assets/chunks/@localSearchIndexroot.CJBoRv1b.js +1 -0
- package/public/docs/assets/chunks/{VPLocalSearchBox.oa8NbaiV.js → VPLocalSearchBox.DtyygpbL.js} +1 -1
- package/public/docs/assets/chunks/{theme.Ca4dR9dL.js → theme.DQS9-w_Z.js} +2 -2
- package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.aQQfP7pj.js → contributor-guide_architecture_detector-contract.md.CsCytCho.js} +1 -1
- package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.aQQfP7pj.lean.js → contributor-guide_architecture_detector-contract.md.CsCytCho.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_overview.md.rhfxoGsI.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_overview.md.rhfxoGsI.lean.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_state-and-history.md.C-xxn0q7.js → contributor-guide_architecture_state-and-history.md.B8Wc_ZTg.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.CBJkwH24.js +6 -0
- package/public/docs/assets/{contributor-guide_architecture_worker-protocol.md.P7d1vtEJ.lean.js → contributor-guide_architecture_worker-protocol.md.CBJkwH24.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_contributing.md.B2E9ni79.js +1 -0
- package/public/docs/assets/contributor-guide_contributing.md.B2E9ni79.lean.js +1 -0
- package/public/docs/assets/contributor-guide_development-setup.md.s85j2l8b.js +15 -0
- package/public/docs/assets/contributor-guide_development-setup.md.s85j2l8b.lean.js +1 -0
- package/public/docs/assets/contributor-guide_testing.md.Cnn6gz1C.js +1 -0
- package/public/docs/assets/contributor-guide_testing.md.Cnn6gz1C.lean.js +1 -0
- package/public/docs/assets/{user-guide_alternative-log-retrieval.md.B4tPGIal.js → user-guide_alternative-log-retrieval.md.ZHPSbBkF.js} +1 -1
- package/public/docs/assets/{user-guide_alternative-log-retrieval.md.B4tPGIal.lean.js → user-guide_alternative-log-retrieval.md.ZHPSbBkF.lean.js} +1 -1
- package/public/docs/assets/user-guide_getting-started.md.Cpe0U-eO.js +3 -0
- package/public/docs/assets/{user-guide_getting-started.md.BJvwLEIM.lean.js → user-guide_getting-started.md.Cpe0U-eO.lean.js} +1 -1
- package/public/docs/assets/{user-guide_mcp-tools.md.DGvolGzt.js → user-guide_mcp-tools.md.DrphooJv.js} +1 -1
- package/public/docs/assets/user-guide_understanding-findings.md.YBBLOwWi.js +1 -0
- package/public/docs/contributor-guide/architecture/board-widgets.html +3 -3
- package/public/docs/contributor-guide/architecture/detector-contract.html +5 -5
- package/public/docs/contributor-guide/architecture/drill-down.html +3 -3
- package/public/docs/contributor-guide/architecture/impact-estimation.html +3 -3
- package/public/docs/contributor-guide/architecture/index.html +3 -3
- package/public/docs/contributor-guide/architecture/overview.html +5 -5
- package/public/docs/contributor-guide/architecture/state-and-history.html +5 -5
- package/public/docs/contributor-guide/architecture/widget-rendering.html +3 -3
- package/public/docs/contributor-guide/architecture/worker-protocol.html +6 -6
- package/public/docs/contributor-guide/contributing.html +5 -5
- package/public/docs/contributor-guide/development-setup.html +11 -8
- package/public/docs/contributor-guide/testing.html +5 -5
- package/public/docs/hashmap.json +1 -1
- package/public/docs/index.html +3 -3
- package/public/docs/tuning-reference/anti-patterns.html +3 -3
- package/public/docs/tuning-reference/aqe.html +3 -3
- package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +3 -3
- package/public/docs/tuning-reference/bottleneck-cold-start.html +3 -3
- package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +3 -3
- package/public/docs/tuning-reference/bottleneck-failures.html +3 -3
- package/public/docs/tuning-reference/bottleneck-gc.html +3 -3
- package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +3 -3
- package/public/docs/tuning-reference/bottleneck-memory-utilization.html +3 -3
- package/public/docs/tuning-reference/bottleneck-retry-waste.html +3 -3
- package/public/docs/tuning-reference/bottleneck-shuffle.html +3 -3
- package/public/docs/tuning-reference/bottleneck-skew.html +3 -3
- package/public/docs/tuning-reference/bottleneck-slow-host.html +3 -3
- package/public/docs/tuning-reference/bottleneck-small-files.html +3 -3
- package/public/docs/tuning-reference/bottleneck-spill.html +3 -3
- package/public/docs/tuning-reference/bottleneck-straggler.html +3 -3
- package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +3 -3
- package/public/docs/tuning-reference/bottleneck-utilization.html +3 -3
- package/public/docs/tuning-reference/caching.html +3 -3
- package/public/docs/tuning-reference/cluster-config.html +3 -3
- package/public/docs/tuning-reference/config.html +3 -3
- package/public/docs/tuning-reference/data-formats.html +3 -3
- package/public/docs/tuning-reference/index.html +3 -3
- package/public/docs/tuning-reference/intro.html +3 -3
- package/public/docs/tuning-reference/joins.html +3 -3
- package/public/docs/tuning-reference/memory-model.html +3 -3
- package/public/docs/tuning-reference/metrics.html +3 -3
- package/public/docs/tuning-reference/partitioning.html +3 -3
- package/public/docs/tuning-reference/pyspark.html +3 -3
- package/public/docs/tuning-reference/shuffle.html +3 -3
- package/public/docs/tuning-reference/spark-architecture.html +3 -3
- package/public/docs/tuning-reference/table-formats.html +3 -3
- package/public/docs/user-guide/alternative-log-retrieval.html +5 -5
- package/public/docs/user-guide/getting-started.html +6 -6
- package/public/docs/user-guide/mcp-tools.html +5 -5
- package/public/docs/user-guide/run-comparison.html +3 -3
- package/public/docs/user-guide/understanding-findings.html +5 -5
- package/public/index.html +2 -2
- package/vendor-core/cli/native-zstd.js +2 -2
- package/vendor-core/detectors.js +16 -2
- package/vendor-core/docs-content/detection/fail.md +6 -2
- package/vendor-core/event-handlers.js +21 -0
- package/vendor-core/event-schemas.js +8 -0
- package/vendor-core/evidence-report.js +20 -1
- package/vendor-core/parser-worker.js +21 -4
- package/vendor-core/redact.js +24 -2
- package/vendor-core/run-comparison.js +8 -0
- package/vendor-core/shs-fetch.js +175 -65
- package/vendor-core/shs-load.js +1 -1
- package/vendor-core/stage-quantiles.js +12 -0
- package/vendor-core/task-failure.js +151 -0
- package/vendor-core/vendor/fflate.js +1 -1
- package/vendor-core/zip-archive.js +167 -0
- package/public/assets/CacheUtilization-DwX7lisd.js +0 -1
- package/public/assets/CachingOpportunity-DUE3GKpx.js +0 -1
- package/public/assets/Dashboard-y3_9iQJo.js +0 -1
- package/public/assets/DuplicatePlanSubtree-BSQZFeNq.js +0 -1
- package/public/assets/GcPressure-CLT5hkMJ.js +0 -1
- package/public/assets/OverBroadcast-C5pF66aq.js +0 -1
- package/public/assets/PartitionSizing-BvEJQ5M1.js +0 -1
- package/public/assets/PlanGraphRoute-DoIGA7_H.js +0 -1
- package/public/assets/SlowHost-D5tWJ1UR.js +0 -1
- package/public/assets/SmallFiles-CTtNiSOh.js +0 -1
- package/public/assets/Spill-BT8my24K.js +0 -1
- package/public/assets/Straggler-BIgxsowI.js +0 -1
- package/public/assets/TaskFailures-B9lLt6Rw.js +0 -1
- package/public/assets/UnderBroadcast-BWqWMpeG.js +0 -1
- package/public/assets/index-CjVklAdW.css +0 -2
- package/public/assets/parser-worker-C3BAO6z9.js +0 -117
- package/public/assets/useRecentFiles-0XuhS91q.js +0 -11
- package/public/docs/assets/chunks/@localSearchIndexroot.BEfYag5Q.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_overview.md.BXQdgG5u.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_overview.md.BXQdgG5u.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.P7d1vtEJ.js +0 -6
- package/public/docs/assets/contributor-guide_contributing.md.CvRsdr6J.js +0 -1
- package/public/docs/assets/contributor-guide_contributing.md.CvRsdr6J.lean.js +0 -1
- package/public/docs/assets/contributor-guide_development-setup.md.DvMV1xhQ.js +0 -12
- package/public/docs/assets/contributor-guide_development-setup.md.DvMV1xhQ.lean.js +0 -1
- package/public/docs/assets/contributor-guide_testing.md.BU6vyA-9.js +0 -1
- package/public/docs/assets/contributor-guide_testing.md.BU6vyA-9.lean.js +0 -1
- package/public/docs/assets/user-guide_getting-started.md.BJvwLEIM.js +0 -3
- package/public/docs/assets/user-guide_understanding-findings.md.B29f0ucS.js +0 -1
- /package/public/docs/assets/{contributor-guide_architecture_state-and-history.md.C-xxn0q7.lean.js → contributor-guide_architecture_state-and-history.md.B8Wc_ZTg.lean.js} +0 -0
- /package/public/docs/assets/{user-guide_mcp-tools.md.DGvolGzt.lean.js → user-guide_mcp-tools.md.DrphooJv.lean.js} +0 -0
- /package/public/docs/assets/{user-guide_understanding-findings.md.B29f0ucS.lean.js → user-guide_understanding-findings.md.YBBLOwWi.lean.js} +0 -0
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from './event-schemas.js';
|
|
20
20
|
import { assertNever } from './assert-never.js';
|
|
21
21
|
import { finalizeStage } from './stage-quantiles.js';
|
|
22
|
+
import { MAX_FAILURE_DETAILS_PER_STAGE, extractTaskFailureDetail, taskFailureKey, } from './task-failure.js';
|
|
22
23
|
import { computeRunAggregates } from './run-aggregates.js';
|
|
23
24
|
|
|
24
25
|
|
|
@@ -94,6 +95,9 @@ const MAX_TASK_SAMPLES = 20;
|
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
97
101
|
|
|
98
102
|
|
|
99
103
|
|
|
@@ -136,6 +140,8 @@ const MAX_TASK_SAMPLES = 20;
|
|
|
136
140
|
|
|
137
141
|
|
|
138
142
|
|
|
143
|
+
|
|
144
|
+
|
|
139
145
|
|
|
140
146
|
|
|
141
147
|
|
|
@@ -485,6 +491,19 @@ function taskRecordToSample(t ) {
|
|
|
485
491
|
};
|
|
486
492
|
}
|
|
487
493
|
|
|
494
|
+
// One shared detail object per distinct failure, so a stage with thousands of failed attempts
|
|
495
|
+
// holds each bounded stack excerpt once. Past the cap an attempt keeps only its reason tag.
|
|
496
|
+
function internTaskFailure(stage , endReason ) {
|
|
497
|
+
const detail = extractTaskFailureDetail(endReason);
|
|
498
|
+
if (!detail || !stage.failureDetails) return null;
|
|
499
|
+
const key = taskFailureKey(detail);
|
|
500
|
+
const known = stage.failureDetails.get(key);
|
|
501
|
+
if (known) return known;
|
|
502
|
+
if (stage.failureDetails.size >= MAX_FAILURE_DETAILS_PER_STAGE) return null;
|
|
503
|
+
stage.failureDetails.set(key, detail);
|
|
504
|
+
return detail;
|
|
505
|
+
}
|
|
506
|
+
|
|
488
507
|
export function accumulateTask(event , state ) {
|
|
489
508
|
const stageId = event['Stage ID'];
|
|
490
509
|
const stage = state.stages.get(stageId);
|
|
@@ -521,6 +540,7 @@ export function accumulateTask(event , state
|
|
|
521
540
|
launchTime: info['Launch Time'] ?? 0,
|
|
522
541
|
finishTime: info['Finish Time'] ?? 0,
|
|
523
542
|
reason: event['Task End Reason']?.['Reason'] ?? null,
|
|
543
|
+
failure: failed ? internTaskFailure(stage, event['Task End Reason']) : null,
|
|
524
544
|
speculative: info['Speculative'] === true,
|
|
525
545
|
host: info['Host'] ?? '',
|
|
526
546
|
executorId: info['Executor ID'] ?? '',
|
|
@@ -802,6 +822,7 @@ export function submitStage(event , st
|
|
|
802
822
|
failureReasons: new Map(),
|
|
803
823
|
stageFailureReason: null,
|
|
804
824
|
taskAttempts: new Map(),
|
|
825
|
+
failureDetails: new Map(),
|
|
805
826
|
retryTaskSamples: [],
|
|
806
827
|
retryWasteMs: 0,
|
|
807
828
|
wastedAttempts: 0,
|
|
@@ -238,6 +238,14 @@ export const TaskEndEventSchema = z.object({
|
|
|
238
238
|
'Stage Attempt ID': z.number().optional(),
|
|
239
239
|
'Task End Reason': z.object({
|
|
240
240
|
Reason: z.string().optional(),
|
|
241
|
+
// The error behind a failed attempt, read by extractTaskFailureDetail (task-failure.ts), which
|
|
242
|
+
// keeps only string values: unknown here so an unexpected shape drops the detail, never the task.
|
|
243
|
+
'Class Name': z.unknown().optional(),
|
|
244
|
+
Description: z.unknown().optional(),
|
|
245
|
+
'Full Stack Trace': z.unknown().optional(),
|
|
246
|
+
'Loss Reason': z.unknown().optional(),
|
|
247
|
+
Message: z.unknown().optional(),
|
|
248
|
+
'Kill Reason': z.unknown().optional(),
|
|
241
249
|
}).optional(),
|
|
242
250
|
'Task Info': z.object({
|
|
243
251
|
'Launch Time': z.number().optional(),
|
|
@@ -6,6 +6,7 @@ import { detectorCatalog } from './detectors.js';
|
|
|
6
6
|
import { typeTag, formatBytes, formatDuration, IMPACT_BAND_ORDER } from './format-utils.js';
|
|
7
7
|
import { FINDING_NAMES, titleCase } from './finding-names.js';
|
|
8
8
|
import { redactReport } from './redact.js';
|
|
9
|
+
import { formatTaskFailureHeadline, } from './task-failure.js';
|
|
9
10
|
import { coreFindingActionLabel } from './finding-action-label.js';
|
|
10
11
|
import { matchesFindingFilterCriteria } from './finding-filter-predicate.js';
|
|
11
12
|
import { buildRecommendationRollup, isEligible, rankFindings, } from './recommendation-rollup.js';
|
|
@@ -269,6 +270,21 @@ function renderEvidenceValue(key , value ) {
|
|
|
269
270
|
return String(value);
|
|
270
271
|
}
|
|
271
272
|
|
|
273
|
+
// The `failures` finding's distinct errors: a headline per group, its stack excerpt as an indented
|
|
274
|
+
// code block (indented, not fenced, so no excerpt content can close it early).
|
|
275
|
+
function renderFailureGroups(groups ) {
|
|
276
|
+
const lines = [` - failureGroups: ${groups.length}`];
|
|
277
|
+
for (const g of groups) {
|
|
278
|
+
lines.push(` - ${g.count} task(s): ${formatTaskFailureHeadline(g)}`);
|
|
279
|
+
if (g.stackExcerpt) {
|
|
280
|
+
lines.push('');
|
|
281
|
+
for (const l of g.stackExcerpt.split('\n')) lines.push(` ${l}`);
|
|
282
|
+
lines.push('');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return lines;
|
|
286
|
+
}
|
|
287
|
+
|
|
272
288
|
function formatWallClockRange(low , high ) {
|
|
273
289
|
const fmtMs = (ms ) => (ms === 0 ? '0s' : formatDuration(ms));
|
|
274
290
|
return low === high ? `Estimated ${fmtMs(high)}` : `Estimated ${fmtMs(low)}-${fmtMs(high)}`;
|
|
@@ -336,7 +352,10 @@ function renderMarkdown(json ) {
|
|
|
336
352
|
const evidence = Object.entries(r.evidence ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
337
353
|
if (evidence.length) {
|
|
338
354
|
lines.push('- evidence:');
|
|
339
|
-
for (const [k, v] of evidence)
|
|
355
|
+
for (const [k, v] of evidence) {
|
|
356
|
+
if (k === 'failureGroups' && Array.isArray(v)) lines.push(...renderFailureGroups(v ));
|
|
357
|
+
else lines.push(` - ${k}: ${renderEvidenceValue(k, v)}`);
|
|
358
|
+
}
|
|
340
359
|
}
|
|
341
360
|
lines.push('');
|
|
342
361
|
}
|
|
@@ -4,7 +4,8 @@ import { Decompress as ZstdDecompress } from './vendor/fzstd.js';
|
|
|
4
4
|
import { createSnappyBlockDecoder } from './snappy-block.js';
|
|
5
5
|
import { createState, dispatchLine, buildChunkDecoder, emitParseCompletion, } from './event-handlers.js';
|
|
6
6
|
import { TASK_FIELD_NAMES } from './stage-quantiles.js';
|
|
7
|
-
import { runParseFromUrl, sniffCodec } from './shs-fetch.js';
|
|
7
|
+
import { runParseFromUrl, sniffCodec, parseZipArchive } from './shs-fetch.js';
|
|
8
|
+
import { isZip } from './zip-archive.js';
|
|
8
9
|
import { createWorkerZstdDecoders } from './zstd-worker-client.js';
|
|
9
10
|
|
|
10
11
|
export {
|
|
@@ -35,6 +36,8 @@ const MIN_PROGRESS_STEPS = 100;
|
|
|
35
36
|
const PROGRESS_EMIT_LINES = 300;
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
const NOT_AN_EVENT_LOG = 'Not a Spark event log: no application-start event found. Choose a Spark event log file, or check the docs for supported formats.';
|
|
40
|
+
|
|
38
41
|
// zstdDecoder replaces the vendored fzstd for zstd input; the Node CLI/MCP path passes
|
|
39
42
|
// cli/native-zstd.ts's native-zlib decoder, which a browser bundle can't import.
|
|
40
43
|
|
|
@@ -140,6 +143,20 @@ export async function runParse(
|
|
|
140
143
|
return;
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
// A Spark History Server download (the UI's download link, or GET
|
|
147
|
+
// /api/v1/applications/<id>/logs) is a zip holding the log file, or a
|
|
148
|
+
// rolling log's parts: unwrap it through the same path the SHS fetch uses.
|
|
149
|
+
if (isZip(new Uint8Array(await file.slice(0, Math.min(4, file.size)).arrayBuffer()))) {
|
|
150
|
+
await parseZipArchive(file, state, emit, {
|
|
151
|
+
zstdDecoder,
|
|
152
|
+
chunkSize,
|
|
153
|
+
progressEvery: PROGRESS_EMIT_LINES,
|
|
154
|
+
reportPct: true,
|
|
155
|
+
onInvalid: (detail) => emit({ type: 'error', message: detail ?? NOT_AN_EVENT_LOG }),
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
const decoder = buildChunkDecoder();
|
|
144
161
|
const joined = [];
|
|
145
162
|
let linesProcessed = 0;
|
|
@@ -168,7 +185,7 @@ export async function runParse(
|
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
if (!state.app) {
|
|
171
|
-
emit({ type: 'error', message:
|
|
188
|
+
emit({ type: 'error', message: NOT_AN_EVENT_LOG });
|
|
172
189
|
return;
|
|
173
190
|
}
|
|
174
191
|
|
|
@@ -227,7 +244,7 @@ export async function runParseFiles(
|
|
|
227
244
|
}
|
|
228
245
|
|
|
229
246
|
if (!state.app) {
|
|
230
|
-
emit({ type: 'error', message:
|
|
247
|
+
emit({ type: 'error', message: NOT_AN_EVENT_LOG });
|
|
231
248
|
return;
|
|
232
249
|
}
|
|
233
250
|
|
|
@@ -242,7 +259,7 @@ if (isWorker) {
|
|
|
242
259
|
let workerState = null;
|
|
243
260
|
// Dropped zstd files decompress in a second worker, overlapping with parsing here. The
|
|
244
261
|
// `new Worker(new URL(...))` stays inline for Vite's worker detection (see ingest.ts). The SHS
|
|
245
|
-
// path (runParseFromUrl)
|
|
262
|
+
// path (runParseFromUrl) keeps in-thread fzstd.
|
|
246
263
|
const zstdDecoder = createWorkerZstdDecoders(
|
|
247
264
|
() => new Worker(new URL('./zstd-worker.js', import.meta.url), { type: 'module' }),
|
|
248
265
|
(onChunk) => new (ZstdDecompress )(onChunk),
|
package/vendor-core/redact.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// and non-mutating (returns a fresh, deep-copied tree).
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
import { redactTaskFailureGroup, } from './task-failure.js';
|
|
11
12
|
|
|
12
13
|
// Host / IP identifier patterns. Used to enumerate host names that surface only
|
|
13
14
|
// inside free text: recommendation strings, `stageFailed`'s failure-reason
|
|
@@ -79,6 +80,25 @@ function collectHostFields(node , hosts ) {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// A failed-task error's message and stack text can carry file paths and data values that no
|
|
84
|
+
// host/app-id pattern recognizes, so they are dropped rather than pseudonymized: every array under
|
|
85
|
+
// a key named `failureGroups` (a `failures` finding's, or a stage's in the HTML export) gets
|
|
86
|
+
// redactTaskFailureGroup applied. Walking by key name, like collectHostFields, needs no path list.
|
|
87
|
+
// Returns a fresh tree.
|
|
88
|
+
function redactFailureGroups (node ) {
|
|
89
|
+
if (Array.isArray(node)) return node.map((n) => redactFailureGroups(n)) ;
|
|
90
|
+
if (node && typeof node === 'object') {
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [k, v] of Object.entries(node)) {
|
|
93
|
+
out[k] = k === 'failureGroups' && Array.isArray(v)
|
|
94
|
+
? v.map((g) => (g && typeof g === 'object' ? redactTaskFailureGroup(g ) : g))
|
|
95
|
+
: redactFailureGroups(v);
|
|
96
|
+
}
|
|
97
|
+
return out ;
|
|
98
|
+
}
|
|
99
|
+
return node;
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
// Spark config keys ending in `host`/`hostname` (e.g. spark.driver.host,
|
|
83
103
|
// spark.yarn.am.hostname) carry plain FQDN host names that neither
|
|
84
104
|
// HOST_PATTERNS matches (no IP/EC2 shape) nor collectHostFields's by-key-name
|
|
@@ -172,7 +192,8 @@ function applyReplacements (node , ids
|
|
|
172
192
|
return deepReplace(node, merged) ;
|
|
173
193
|
}
|
|
174
194
|
|
|
175
|
-
export function redactReport (
|
|
195
|
+
export function redactReport (input ) {
|
|
196
|
+
const report = redactFailureGroups(input);
|
|
176
197
|
const { appIds, hosts } = collectIds(report);
|
|
177
198
|
return applyReplacements(report, { appIds, hosts });
|
|
178
199
|
}
|
|
@@ -216,7 +237,8 @@ export function redactComparison (comparison ) {
|
|
|
216
237
|
// this also walks executors.added/removed for their literal `host` field
|
|
217
238
|
// (ExecutorAddedEvent.host), since raw executor records: not just findings
|
|
218
239
|
//: reach data.js.
|
|
219
|
-
export function redactExportData(
|
|
240
|
+
export function redactExportData(input ) {
|
|
241
|
+
const data = redactFailureGroups(input);
|
|
220
242
|
const appIds = new Set ();
|
|
221
243
|
const hosts = new Set ();
|
|
222
244
|
const appId = data.app?.id;
|
|
@@ -180,6 +180,14 @@ function skewRatios(stages ) {
|
|
|
180
180
|
return out;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Every key metricDeltas() emits, in emission order. The CLI validates
|
|
184
|
+
// --regression-metric against it, so a misspelled key is a usage error rather
|
|
185
|
+
// than an inconclusive budget. A contract test keeps it in sync.
|
|
186
|
+
export const COMPARISON_METRIC_KEYS = [
|
|
187
|
+
'wallClock', 'shuffleSpill', 'taskSkew', 'failedTaskRate', 'diskSpill', 'gcTime',
|
|
188
|
+
'inputBytes', 'outputBytes', 'executorRunTime', 'taskCount', 'executorsAdded',
|
|
189
|
+
];
|
|
190
|
+
|
|
183
191
|
// Volume/count metrics, not cost metrics: more or less input/output data, or
|
|
184
192
|
// tasks/executors, isn't inherently better or worse (it may just reflect a
|
|
185
193
|
// differently-sized job), unlike wall-clock, spill, GC, etc. Exported as the
|
package/vendor-core/shs-fetch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Gunzip } from './vendor/fflate.js';
|
|
2
2
|
import { createLz4BlockDecoder } from './lz4-block.js';
|
|
3
3
|
import { Decompress as ZstdDecompress } from './vendor/fzstd.js';
|
|
4
4
|
import { createSnappyBlockDecoder } from './snappy-block.js';
|
|
@@ -6,6 +6,8 @@ import { buildProxyRequestUrl, isShsErrorCode } from './shs-request.js';
|
|
|
6
6
|
import { dispatchLine, buildChunkDecoder, emitParseCompletion, } from './event-handlers.js';
|
|
7
7
|
import { ShsProxyErrorBodySchema } from './shs-schemas.js';
|
|
8
8
|
import { naturalCompare, reassembleRollingEntries } from './rolling-log-reassembly.js';
|
|
9
|
+
import { listZipEntries, streamZipEntry, } from './zip-archive.js';
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
export { naturalCompare, reassembleRollingEntries };
|
|
11
13
|
|
|
@@ -24,48 +26,84 @@ export function sniffCodec(bytes )
|
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// whole decompressed entry as one buffer. A giant
|
|
30
|
-
// (or gzip member) declares its full content size in
|
|
31
|
-
// it (fzstd's `decompress()`, fflate's `gunzipSync()`)
|
|
32
|
-
// size as a single ArrayBuffer up front, which can exceed
|
|
33
|
-
// will allocate for a multi-GB event log ("Array buffer
|
|
29
|
+
// Decodes one zip entry's (possibly compressed) bytes as they stream out of
|
|
30
|
+
// the archive, block by block, invoking `onChunk` per decompressed piece;
|
|
31
|
+
// never materializes the whole decompressed entry as one buffer. A giant
|
|
32
|
+
// single-segment zstd frame (or gzip member) declares its full content size in
|
|
33
|
+
// the header; one-shotting it (fzstd's `decompress()`, fflate's `gunzipSync()`)
|
|
34
|
+
// allocates that entire size as a single ArrayBuffer up front, which can exceed
|
|
35
|
+
// what the browser will allocate for a multi-GB event log ("Array buffer
|
|
36
|
+
// allocation failed"). The codec is sniffed from the entry's first 8 bytes,
|
|
37
|
+
// falling back to the entry name's suffix.
|
|
34
38
|
// fflate's Gunzip and fzstd's Decompress are untyped vendor JS, so TS can't
|
|
35
39
|
// infer a construct signature for them; this local shape types the call sites
|
|
36
40
|
// without touching the vendored files.
|
|
37
41
|
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
function decodeEntry(
|
|
45
|
-
name , raw , onChunk , zstdDecoder ,
|
|
46
|
-
) {
|
|
47
|
-
const codec = sniffCodec(raw);
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
function openCodecSink(
|
|
46
|
+
codec , name , onChunk , zstdDecoder ,
|
|
47
|
+
) {
|
|
48
48
|
if (codec === 'lz4' || name.endsWith('.lz4')) {
|
|
49
49
|
const lz4 = createLz4BlockDecoder(onChunk);
|
|
50
|
-
lz4.push(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
new (Gunzip )(onChunk)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
return { push(chunk, final) { lz4.push(chunk); if (final) lz4.end(); } };
|
|
51
|
+
}
|
|
52
|
+
if (codec === 'gz' || name.endsWith('.gz')) {
|
|
53
|
+
const gunzip = new (Gunzip )(onChunk);
|
|
54
|
+
return { push: (chunk, final) => gunzip.push(chunk, final) };
|
|
55
|
+
}
|
|
56
|
+
if (codec === 'zstd' || name.endsWith('.zstd') || name.endsWith('.zst')) {
|
|
57
|
+
const zstd = zstdDecoder ? zstdDecoder(onChunk) : new (ZstdDecompress )(onChunk);
|
|
58
|
+
return { push: (chunk, final) => zstd.push(chunk, final), cancel: () => (zstd ).cancel?.() };
|
|
59
|
+
}
|
|
60
|
+
if (codec === 'snappy' || name.endsWith('.snappy')) {
|
|
57
61
|
const snappy = createSnappyBlockDecoder(onChunk);
|
|
58
|
-
snappy.push(
|
|
59
|
-
snappy.end();
|
|
60
|
-
} else {
|
|
61
|
-
onChunk(raw);
|
|
62
|
+
return { push(chunk, final) { snappy.push(chunk); if (final) snappy.end(); } };
|
|
62
63
|
}
|
|
64
|
+
return { push: (chunk) => onChunk(chunk) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Wraps openCodecSink for a stream of unknown length: holds back the latest
|
|
68
|
+
// chunk so the last one can be pushed with `final` set (codecs reject a
|
|
69
|
+
// trailing empty push less uniformly than a real last chunk), and buffers the
|
|
70
|
+
// start of the entry until it has the 8 bytes sniffCodec needs.
|
|
71
|
+
function createEntryDecoder(name , onChunk , zstdDecoder ) {
|
|
72
|
+
let head = new Uint8Array(0);
|
|
73
|
+
let held = null;
|
|
74
|
+
let sink = null;
|
|
75
|
+
const open = () => {
|
|
76
|
+
sink = openCodecSink(sniffCodec(head), name, onChunk, zstdDecoder);
|
|
77
|
+
held = head;
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
async push(chunk ) {
|
|
81
|
+
if (!sink) {
|
|
82
|
+
const joined = new Uint8Array(head.length + chunk.length);
|
|
83
|
+
joined.set(head);
|
|
84
|
+
joined.set(chunk, head.length);
|
|
85
|
+
head = joined;
|
|
86
|
+
if (head.length >= 8) open();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await sink.push(held , false);
|
|
90
|
+
held = chunk;
|
|
91
|
+
},
|
|
92
|
+
async end() {
|
|
93
|
+
if (!sink) {
|
|
94
|
+
if (!head.length) return;
|
|
95
|
+
open();
|
|
96
|
+
}
|
|
97
|
+
await sink .push(held , true);
|
|
98
|
+
},
|
|
99
|
+
cancel() { sink?.cancel?.(); },
|
|
100
|
+
};
|
|
63
101
|
}
|
|
64
102
|
|
|
65
103
|
|
|
66
104
|
|
|
67
|
-
function emitShsError(emit , code ) {
|
|
68
|
-
emit({ type: 'error', source: 'shs', code });
|
|
105
|
+
function emitShsError(emit , code , message ) {
|
|
106
|
+
emit({ type: 'error', source: 'shs', code, ...(message ? { message } : {}) });
|
|
69
107
|
}
|
|
70
108
|
|
|
71
109
|
export async function runParseFromUrl(
|
|
@@ -141,55 +179,127 @@ export async function runParseFromUrl(
|
|
|
141
179
|
if (total) {
|
|
142
180
|
emit({ type: 'progress', pct: 0.5, linesProcessed: 0 });
|
|
143
181
|
}
|
|
144
|
-
decodeShsArchive(zipBytes, state, emit);
|
|
182
|
+
await decodeShsArchive(zipBytes, state, emit);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Reads the in-memory SHS download through the same random-access interface a
|
|
186
|
+
// dropped File offers, so both paths share parseZipArchive.
|
|
187
|
+
function bytesZipSource(bytes ) {
|
|
188
|
+
return {
|
|
189
|
+
size: bytes.length,
|
|
190
|
+
slice: (start, end) => ({ arrayBuffer: async () => bytes.slice(start, end).buffer }),
|
|
191
|
+
};
|
|
145
192
|
}
|
|
146
193
|
|
|
147
194
|
export function decodeShsArchive(
|
|
148
195
|
zipBytes , state , emit , { zstdDecoder } = {},
|
|
149
|
-
)
|
|
150
|
-
|
|
196
|
+
) {
|
|
197
|
+
return parseZipArchive(bytesZipSource(zipBytes), state, emit, {
|
|
198
|
+
zstdDecoder,
|
|
199
|
+
onInvalid: (detail) => emitShsError(emit, 'invalid-event-log', detail ?? undefined),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const baseName = (e ) => e.name.slice(e.name.lastIndexOf('/') + 1);
|
|
204
|
+
const isRollingPart = (e ) => /^events_\d+_/.test(baseName(e));
|
|
205
|
+
|
|
206
|
+
// The application attempts a History Server zip holds. Without an attempt ID
|
|
207
|
+
// the History Server packs every attempt into one download: each single-file
|
|
208
|
+
// log is its own root entry, each rolling log its own `eventlog_v2_` directory.
|
|
209
|
+
// Directory entries and `appstatus` markers are not attempts.
|
|
210
|
+
function countLogAttempts(entries ) {
|
|
211
|
+
const attempts = new Set ();
|
|
212
|
+
for (const e of entries) {
|
|
213
|
+
if (e.name.endsWith('/') || baseName(e).toLowerCase().startsWith('appstatus')) continue;
|
|
214
|
+
const dir = e.name.slice(0, e.name.indexOf('/') + 1);
|
|
215
|
+
attempts.add(dir || (isRollingPart(e) ? '' : e.name));
|
|
216
|
+
}
|
|
217
|
+
return attempts.size;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The event-log entries a single-attempt History Server zip holds, in parse
|
|
221
|
+
// order. A rolling log's parts sit under an `eventlog_v2_<appId>/` directory
|
|
222
|
+
// entry; they are matched and reassembled by base name. A single-file log is
|
|
223
|
+
// one entry at the root. Directory entries and the rolling log's `appstatus`
|
|
224
|
+
// marker are skipped.
|
|
225
|
+
function selectLogEntries(entries ) {
|
|
226
|
+
const files = entries.filter((e) => !e.name.endsWith('/'));
|
|
227
|
+
if (files.some(isRollingPart)) {
|
|
228
|
+
const byBase = new Map(files.map((e) => [baseName(e), e]));
|
|
229
|
+
return reassembleRollingEntries(files.map(baseName)).map((n) => byBase.get(n) );
|
|
230
|
+
}
|
|
231
|
+
return files.filter((e) => baseName(e).toLowerCase() !== 'appstatus');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
// Parses a single-entry or rolling Spark History Server zip, streaming each
|
|
248
|
+
// entry out of `source` in `chunkSize` slices. One NDJSON line decoder spans
|
|
249
|
+
// all entries, since a file-roll boundary need not fall on a line boundary.
|
|
250
|
+
export async function parseZipArchive(
|
|
251
|
+
source ,
|
|
252
|
+
state ,
|
|
253
|
+
emit ,
|
|
254
|
+
{ zstdDecoder, chunkSize = 512 * 1024, onInvalid, progressEvery = 2000, reportPct = false } ,
|
|
255
|
+
) {
|
|
256
|
+
let logEntries ;
|
|
257
|
+
let attempts ;
|
|
151
258
|
try {
|
|
152
|
-
entries =
|
|
153
|
-
|
|
154
|
-
|
|
259
|
+
const entries = await listZipEntries(source);
|
|
260
|
+
attempts = countLogAttempts(entries);
|
|
261
|
+
logEntries = attempts > 1 ? [] : selectLogEntries(entries);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
onInvalid(`Could not read the zip archive: ${e instanceof Error ? e.message : String(e)}`);
|
|
155
264
|
return;
|
|
156
265
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (isRolling) {
|
|
161
|
-
try {
|
|
162
|
-
names = reassembleRollingEntries(allNames);
|
|
163
|
-
} catch {
|
|
164
|
-
emitShsError(emit, 'invalid-event-log');
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
names = allNames.filter(n => n.toLowerCase() !== 'appstatus').sort(naturalCompare);
|
|
266
|
+
if (attempts > 1) {
|
|
267
|
+
onInvalid(`The zip archive holds ${attempts} application attempts. Download a single attempt, for example GET /api/v1/applications/<appId>/<attemptId>/logs.`);
|
|
268
|
+
return;
|
|
169
269
|
}
|
|
170
|
-
if (
|
|
171
|
-
|
|
270
|
+
if (logEntries.length === 0) {
|
|
271
|
+
onInvalid('The zip archive contains no event log.');
|
|
172
272
|
return;
|
|
173
273
|
}
|
|
174
274
|
|
|
275
|
+
const totalBytes = logEntries.reduce((sum, e) => sum + e.compressedSize, 0);
|
|
276
|
+
let bytesRead = 0;
|
|
175
277
|
const decoder = buildChunkDecoder();
|
|
176
278
|
const joined = [];
|
|
177
279
|
let linesProcessed = 0;
|
|
178
|
-
|
|
280
|
+
const feed = (bytes ) => {
|
|
281
|
+
joined.length = 0;
|
|
282
|
+
const lines = decoder.decode(bytes, joined);
|
|
283
|
+
for (let i = 0, j = 0; i < lines.length; i++) {
|
|
284
|
+
dispatchLine(lines[i], state, emit, joined[j]?.index === i ? joined[j++] : undefined);
|
|
285
|
+
linesProcessed++;
|
|
286
|
+
if (linesProcessed % progressEvery === 0) {
|
|
287
|
+
emit({ type: 'progress', pct: reportPct && totalBytes ? bytesRead / totalBytes : null, linesProcessed });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
for (const entry of logEntries) {
|
|
293
|
+
const entryDecoder = createEntryDecoder(entry.name, feed, zstdDecoder);
|
|
179
294
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}, zstdDecoder);
|
|
191
|
-
} catch {
|
|
192
|
-
emitShsError(emit, 'invalid-event-log');
|
|
295
|
+
await streamZipEntry(source, entry, (chunk) => entryDecoder.push(chunk), {
|
|
296
|
+
chunkSize,
|
|
297
|
+
onRead: (bytes) => { bytesRead += bytes; },
|
|
298
|
+
});
|
|
299
|
+
await entryDecoder.end();
|
|
300
|
+
} catch (e) {
|
|
301
|
+
entryDecoder.cancel();
|
|
302
|
+
onInvalid(`Could not decompress "${entry.name}" in the zip archive: ${e instanceof Error ? e.message : String(e)}`);
|
|
193
303
|
return;
|
|
194
304
|
}
|
|
195
305
|
}
|
|
@@ -198,7 +308,7 @@ export function decodeShsArchive(
|
|
|
198
308
|
}
|
|
199
309
|
|
|
200
310
|
if (!state.app) {
|
|
201
|
-
|
|
311
|
+
onInvalid(null);
|
|
202
312
|
return;
|
|
203
313
|
}
|
|
204
314
|
emitParseCompletion(state, emit, linesProcessed);
|
package/vendor-core/shs-load.js
CHANGED
|
@@ -19,7 +19,7 @@ export const DEFAULT_IDLE_TIMEOUT_MS = envInt('SPARKFORENSICS_SHS_TIMEOUT_MS', 3
|
|
|
19
19
|
|
|
20
20
|
function collectShsAppModel(zipBytes ) {
|
|
21
21
|
return collectViaDispatch(
|
|
22
|
-
(state, emit) => decodeShsArchive(zipBytes, state, emit, nodeArchiveCodecs),
|
|
22
|
+
(state, emit, reject) => { decodeShsArchive(zipBytes, state, emit, nodeArchiveCodecs).catch(reject); },
|
|
23
23
|
(msg) => {
|
|
24
24
|
const m = msg ;
|
|
25
25
|
return mcpError(m?.code ?? 'invalid-event-log', m?.message ?? 'Failed to decode SHS archive.');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
|
|
2
|
+
|
|
2
3
|
|
|
3
4
|
// Single source of truth for the packed per-task numeric array: FIELDS (offset
|
|
4
5
|
// constants), TASK_FIELD_NAMES (display labels), and the finalizeStage hot-loop
|
|
@@ -74,6 +75,8 @@ export function finalizeStage(
|
|
|
74
75
|
const executorStats = new Map();
|
|
75
76
|
const failureReasons = new Map();
|
|
76
77
|
const failedTaskSamples = [];
|
|
78
|
+
// Keyed by the interned detail object: accumulateTask shares one per distinct failure.
|
|
79
|
+
const failureGroups = new Map ();
|
|
77
80
|
const localityStats = new Map();
|
|
78
81
|
let peakExecutionMemoryMax = 0;
|
|
79
82
|
|
|
@@ -82,6 +85,8 @@ export function finalizeStage(
|
|
|
82
85
|
if (t.failed) {
|
|
83
86
|
failedTasks++;
|
|
84
87
|
if (t.reason) failureReasons.set(t.reason, (failureReasons.get(t.reason) ?? 0) + 1);
|
|
88
|
+
const failure = (t ).failure;
|
|
89
|
+
if (failure) failureGroups.set(failure, (failureGroups.get(failure) ?? 0) + 1);
|
|
85
90
|
if (failedTaskSamples.length < MAX_TASK_SAMPLES) {
|
|
86
91
|
failedTaskSamples.push({
|
|
87
92
|
taskId: t.taskId, attemptNumber: t.attemptNumber, host: t.host, executorId: t.executorId,
|
|
@@ -125,6 +130,7 @@ export function finalizeStage(
|
|
|
125
130
|
stage.failedTasks = failedTasks;
|
|
126
131
|
stage.speculativeTasks = speculativeTasks;
|
|
127
132
|
stage.taskAttempts = null; // no longer needed after finalize, freeing memory
|
|
133
|
+
stage.failureDetails = null;
|
|
128
134
|
|
|
129
135
|
const arr = new Float64Array(buf);
|
|
130
136
|
state.taskStore.set(stageId, arr);
|
|
@@ -164,6 +170,10 @@ export function finalizeStage(
|
|
|
164
170
|
const failureReasonsArr = [...failureReasons.entries()].map(
|
|
165
171
|
([reason, count]) => ({ reason, count })
|
|
166
172
|
);
|
|
173
|
+
// Most frequent first; ties keep first-seen order (Array.prototype.sort is stable).
|
|
174
|
+
const failureGroupsArr = [...failureGroups.entries()]
|
|
175
|
+
.map(([detail, count]) => ({ ...detail, count }))
|
|
176
|
+
.sort((a, b) => b.count - a.count);
|
|
167
177
|
const localityStatsArr = [...localityStats.entries()].map(
|
|
168
178
|
([locality, count]) => ({ locality, count })
|
|
169
179
|
);
|
|
@@ -173,6 +183,7 @@ export function finalizeStage(
|
|
|
173
183
|
hostStats: hostStatsArr, executorStats: executorStatsArr, failureReasons: failureReasonsArr, localityStats: localityStatsArr, stragglerCount, stragglerExcessMs, longestNonStragglerMs,
|
|
174
184
|
tailReplayRecoveryMs,
|
|
175
185
|
failedTaskSamples,
|
|
186
|
+
failureGroups: failureGroupsArr,
|
|
176
187
|
peakExecutionMemoryMax,
|
|
177
188
|
taskActiveMs: computeTaskActiveMs(arr),
|
|
178
189
|
peakConcurrentTasks,
|
|
@@ -187,6 +198,7 @@ export function finalizeStage(
|
|
|
187
198
|
stageType: acc.shuffleReadBytes > 0 ? 'REDUCE' : 'MAP',
|
|
188
199
|
};
|
|
189
200
|
delete data.taskAttempts; // internal-only field, already nulled above; never part of the public message
|
|
201
|
+
delete data.failureDetails; // internal-only intern table, summarized by failureGroups
|
|
190
202
|
|
|
191
203
|
return { type: 'stage', data };
|
|
192
204
|
}
|