Skip to content

feat(utilities): add HoodieSparkEventLogAnalyzer for Hudi-aware Spark stage analysis - #20196

Draft
nsivabalan wants to merge 2 commits into
apache:masterfrom
nsivabalan:event-log-analyzer-pr
Draft

nsivabalan wants to merge 2 commits into
apache:masterfrom
nsivabalan:event-log-analyzer-pr

Conversation

@nsivabalan

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Part of #19262 (Hudi-aware agent skills and tools), under the Agentic Lakehouse umbrella #19256;
plan in #19262 (comment).

A user looking at a slow Hudi write sees Spark stages: "stage 14, 16000 tasks, 3 minutes". Spark
cannot tell them that stage 14 is bloom-index probing, that its parallelism comes from
hoodie.bloom.index.parallelism, or that the 10,000x task skew there means their key distribution
defeats range pruning. Generic Spark profilers have the stage metrics but no idea which part of Hudi
each stage belongs to, so their advice stays generic.

Hudi already labels its own Spark jobs — HoodieSparkEngineContext.setJobStatus sets a job
description on every significant step. This change reads those labels back out of a Spark event log
and turns a stage listing into a Hudi-phase breakdown.

Summary and Changelog

Adds HoodieSparkEventLogAnalyzer plus a small eventlog package. It parses a Spark event log
(the file written when spark.eventLog.enabled=true) and reports where a Hudi write spent its time,
by Hudi phase and operation rather than by stage number.

No Spark session required — it reads a file, so it runs as a plain java -cp process. Reads
through HoodieStorage, so local, S3 and GCS paths all work, as do rolling-log directories and
gzip. The log is parsed with Jackson rather than Spark's JsonProtocol, which is private and
version-coupled.

Phases. Nine buckets, deliberately coarse, because the useful answer is "60% of your time is
index tagging", not a per-stage taxonomy:

  • Write operations: SOURCE_READ_AND_TRANSFORM, DEDUP_AND_INDEX_TAGGING, DATA_TABLE_WRITE,
    MARKER_RECONCILIATION, METADATA_TABLE_WRITE
  • Table services: PLANNING, EXECUTION
  • Plus ARCHIVAL and UNKNOWN

Two bucket names state an attribution limit rather than hiding it. SOURCE_READ_AND_TRANSFORM is
named for everything it contains because Hudi tags neither the transformer nor record creation, so
their cost is forced inside the source-read stage and cannot be separated. DEDUP_AND_INDEX_TAGGING
likewise covers one job: Hudi's Building workload profile: is the description under which dedup,
index tagging and profiling all execute.

A finer subPhase is kept alongside each stage — recordIndexLookup, bloomIndexLookup,
smallFileProbe, commitStats, compactionPlan and others. That detail is what makes a coarse
bucket actionable: a DATA_TABLE_WRITE row at 57% says little, but the same row tagged
[smallFileProbe] with a 10,000x skew points straight at a pathological partition.

Operations. Thirteen values (WRITE_OPERATION, COMPACTION, CLUSTERING, CLEANING,
ROLLBACK, RESTORE, SAVEPOINT, …) resolved from the module half of the job description, falling
back to phase-sequence inference, then UNKNOWN. The resolution source is reported so a reader can
judge it. This matters because PLANNING and EXECUTION recur across every table service — only
the operation tells a compaction plan from a clustering plan. Repeated rollback groups are promoted
to RESTORE, which is what a restore looks like from the outside.

Two wire encodings. Before HUDI-8596, setJobStatus used setJobGroup, so the module landed in
spark.jobGroup.id and the description held the bare activity. After it, both are combined into the
description. Both are handled, so the tool works on pre-1.0 logs too.

Per stage: task count, duration percentiles, skew ratio, GC fraction, shuffle, spill, failure
reasons. Per application: critical path, peak concurrent executors, and the share of wall clock
per phase and per operation. TABLE output for humans, --output JSON for machines, with a
documented schema and deterministic ordering.

Caveats are part of the output, not a footnote. Hudi sets a job description before the Spark
action and Spark evaluates lazily, so a bucket names where work was forced, not where it logically
belongs. The JSON carries a caveats[] array and the table output has a
"How far to trust the attribution" section; entries fire only for the buckets actually present in
that log.

Known limitation, stated plainly in the docs. With hoodie.metadata.streaming.write.enabled
(the default on Spark), the streaming metadata-write path sets no job description at all, so a large
share of the DAG cannot be attributed. Measured on a local three-way comparison — same workload and
record-level index, toggling only that config — unattributed wall clock went from 3.9% to 60.5%. The
tool reports this honestly rather than guessing, and the breakdown is accurate for non-streaming
writes and for 0.x-style disjoint DAGs.

Also documented: outputBytes and outputRecords are structurally zero for Hudi writes, because
Hudi writes Parquet through its own file handles and Spark never observes the bytes. Commit metadata
is the right source for written volume.

Tests — TestHoodieSparkEventLogAnalyzer, 118 tests on synthetic event logs: both wire
encodings, every phase, operation resolution by module and by sequence, rollback-to-restore
promotion, sub-phase refinement, percentile and skew arithmetic, zero-task stages, stage retries,
truncated and garbage logs, rolling directories, gzip, exit codes, and JSON determinism.

Validated end to end against real event logs from local Hudi writes across several metadata-table
configurations, and against a production Spark UI breakdown.

Impact

Purely additive. New class and new package in hudi-utilities; no existing file is modified. No
public API change, no storage format change, no new hoodie.* configs. Read-only: it parses a log
file and prints a report.

Risk Level

none

Documentation Update

HoodieSparkEventLogAnalyzer.md covers invocation, the phase and operation vocabularies, the JSON
schema, and the caveats and limitations above.

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable

… stage analysis

Summarises a Spark event log by Hudi operation and phase, so a stage report
reads in Hudi's own vocabulary. A generic Spark profiler can say stage 14 was
slow with 16000 tasks; it cannot say stage 14 was bloom-index probing inside an
upsert, so its advice stays generic. This tool reads the job description Hudi
publishes via HoodieEngineContext#setJobStatus and maps it onto a named
operation and phase.

The model is two levels, operation then phase, because a phase alone is
ambiguous: a write inside an upsert is not the write inside a compaction, and
forty repetitions of the rollback phases is one restore, not forty rollbacks.
The operation is derived from the module half of the job description where it
names one, otherwise from the phase sequence, and which signal was used is
reported so a reader can judge confidence. Restore is detected by counting
repeated rollbacks, because BaseRestoreActionExecutor sets no job status and
simply calls the ordinary rollback path for each instant.

Phases are deliberately coarse. The question worth answering is where the time
went, at a granularity someone can act on: five buckets for a write operation,
PLANNING and EXECUTION for every table service, and ARCHIVAL. The finer label is
carried as a sub-phase so nothing is lost, and the verbatim job description is
always reported as the escape hatch.

"Building workload profile:" is the job under which deduplication, index tagging
and profiling all run: Hudi sets the description once and the whole lazy chain
executes beneath it. It therefore belongs to the tagging bucket rather than the
write. The Spark stage name, which names the class that created the RDD, refines
the sub-phase within that bucket so the composition stays visible -- a record
index lookup and a bloom lookup are distinguishable there. The description
always decides the phase; the stage name only adds detail, and is used to decide
a phase only where there is no description at all.

Parallel listings are attributed to the metadata table only when the listed
paths are under it; a data-table listing is left unattributed rather than
guessed at.

Several bucket names state an attribution limit rather than hiding it. Hudi
leaves the transformer, record creation and deduplication untagged, and Spark
evaluates lazily, so their cost is forced into a neighbouring stage and cannot
be separated from a log. SOURCE_READ_AND_TRANSFORM and DEDUP_AND_INDEX_TAGGING
are named for everything they contain, and a caveats list names the buckets in
each log that are prone to absorbing other work, so a downstream optimizer does
not read a single share as ground truth.

One limitation is called out rather than papered over: with
hoodie.metadata.streaming.write.enabled, Hudi's default on Spark, the metadata
write path runs with no job status at all and accounted for 61% of stage wall
clock on a measured run. Those stages stay UNKNOWN, carry a sub-phase so a
reader can see what the unattributed work is, and raise a caveat naming the
config. The tool is accurate for non-streaming writes and 0.x-style DAGs and
degrades on the 1.x streaming default.

Both job-description encodings are supported, because logs of both are in
circulation: the current "module:activity" in spark.job.description, and the
pre-HUDI-8596 form where setJobGroup put the module in spark.jobGroup.id and the
bare activity in the description.

The tool is read-only and needs no SparkContext, so it runs as a plain java -cp
entry point. Paths go through HoodieStorage, so s3/gcs/hdfs work. Parsing uses
Jackson directly rather than Spark's JsonProtocol, which is private and
version-coupled; unknown events are ignored, missing fields default, and a
truncated final line is warned about rather than thrown on, since logs from
killed applications routinely end mid-line.

Output is TABLE for humans or JSON for machines. The JSON envelope is a stable
contract carrying schemaVersion, application, operationSummary, phaseSummary,
stages, caveats and warnings, with deterministic ordering; it is documented in
HoodieSparkEventLogAnalyzer.md next to the tool, including the fact that Spark's
output metrics are structurally zero for Hudi writes because Hudi writes Parquet
through its own file handles.

Exit codes are 0 on a successful parse and 2 for bad arguments or an unreadable
log, including a bad flag.
@codecov-commenter

codecov-commenter commented Oct 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.48126% with 178 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.79%. Comparing base (60caf5f) to head (21e4d07).
⚠️ Report is 58 commits behind head on master.

Files with missing lines Patch % Lines
...apache/hudi/utilities/eventlog/EventLogParser.java 75.25% 42 Missing and 32 partials ⚠️
...he/hudi/utilities/HoodieSparkEventLogAnalyzer.java 81.08% 41 Missing and 15 partials ⚠️
...ache/hudi/utilities/eventlog/EventLogAnalysis.java 84.66% 6 Missing and 19 partials ⚠️
...hudi/utilities/eventlog/HudiOperationResolver.java 88.54% 6 Missing and 5 partials ⚠️
...che/hudi/utilities/eventlog/HudiPhaseResolver.java 95.80% 1 Missing and 5 partials ⚠️
...org/apache/hudi/utilities/eventlog/StageStats.java 95.04% 0 Missing and 6 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #20196      +/-   ##
============================================
+ Coverage     80.49%   80.79%   +0.30%     
- Complexity    34953    35610     +657     
============================================
  Files          2549     2570      +21     
  Lines        143060   145290    +2230     
  Branches      17392    18252     +860     
============================================
+ Hits         115151   117383    +2232     
+ Misses        20004    19852     -152     
- Partials       7905     8055     +150     
Components Coverage Δ
hudi-common 84.19% <ø> (+0.19%) ⬆️
hudi-client 83.95% <ø> (+0.41%) ⬆️
hudi-flink 86.08% <ø> (+0.11%) ⬆️
hudi-spark-datasource 74.19% <ø> (+0.34%) ⬆️
hudi-utilities 78.88% <84.48%> (+0.69%) ⬆️
hudi-cli 70.80% <ø> (+0.02%) ⬆️
hudi-hadoop 71.18% <ø> (+0.14%) ⬆️
hudi-sync 76.85% <ø> (+0.61%) ⬆️
hudi-io 81.57% <ø> (+0.07%) ⬆️
hudi-timeline-service 84.06% <ø> (+0.75%) ⬆️
hudi-cloud 81.01% <ø> (+0.01%) ⬆️
hudi-kafka-connect 57.90% <ø> (+4.70%) ⬆️
Flag Coverage Δ
common-and-other-modules 52.26% <0.00%> (-0.12%) ⬇️
flink-integration-tests 49.65% <ø> (+0.09%) ⬆️
hadoop-mr-java-client 43.94% <ø> (-0.04%) ⬇️
integration-tests 13.30% <0.00%> (-0.16%) ⬇️
spark-client-hadoop-common 38.16% <0.00%> (-0.49%) ⬇️
spark-java-tests 52.07% <0.00%> (-0.34%) ⬇️
spark-scala-tests 47.09% <0.00%> (+<0.01%) ⬆️
utilities 37.46% <84.48%> (+0.60%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
.../apache/hudi/utilities/eventlog/HudiOperation.java 100.00% <100.00%> (ø)
.../org/apache/hudi/utilities/eventlog/HudiPhase.java 100.00% <100.00%> (ø)
...che/hudi/utilities/eventlog/HudiPhaseResolver.java 95.80% <95.80%> (ø)
...org/apache/hudi/utilities/eventlog/StageStats.java 95.04% <95.04%> (ø)
...hudi/utilities/eventlog/HudiOperationResolver.java 88.54% <88.54%> (ø)
...ache/hudi/utilities/eventlog/EventLogAnalysis.java 84.66% <84.66%> (ø)
...he/hudi/utilities/HoodieSparkEventLogAnalyzer.java 81.08% <81.08%> (ø)
...apache/hudi/utilities/eventlog/EventLogParser.java 75.25% <75.25%> (ø)

... and 160 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot added the size:XL PR with lines of changes > 1000 label Oct 4, 2026
@hudi-bot

hudi-bot commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

@nsivabalan
nsivabalan marked this pull request as draft October 8, 2026 03:09
… and add two attribution caveats

- The analyzer opens no SparkContext but still needs Spark's jars on the classpath (Jackson among
  them); a bare `java -cp` on the utilities bundle fails with NoClassDefFoundError. The doc now
  shows spark-submit and says why.
- New caveat under DEDUP_AND_INDEX_TAGGING: there is no simpleIndexLookup subPhase, because
  HoodieSimpleIndex builds its join through generic HoodieJavaRDD call sites; a stage whose planned
  task count equals the table's base-file count is the simple-index join regardless of its label.
- New caveat under DATA_TABLE_WRITE: the stage that writes the data files is usually forced by the
  commit action and carries the commitStats subPhase; one task per written file group and most of
  the bucket's executor time identifies it as the write.

Both observed on a replayed upsert against a 250-file table: three 250-task stages at 51% of stage
wall clock labelled workloadProfile, and a 240-task, 70 s write stage labelled commitStats.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL PR with lines of changes > 1000

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants