Lab 01 — Codebase Spelunking
Phase: 02 — JVM Codebase Navigation
Difficulty: ⭐⭐☆☆☆ | Estimated Time: 4–6 hours
Type: Code Navigation / Document
Repository: apache/spark — tag v3.5.1
Goal
Produce an annotated execution trace of spark.read.csv("path") from the first line of user-facing API code down to FileScanRDD.compute(). Map the Maven module dependencies touched along the way. Explain the logical→physical plan boundary in your own words.
After this lab you will be able to open any Apache Spark bug report and navigate to the relevant code in under 10 minutes.
Setup (Do This Once)
# 1. Clone the repository
git clone https://github.com/apache/spark.git
cd spark
git checkout v3.5.1
# 2. Build the modules used in this lab (skip tests to save time)
./build/mvn -DskipTests -Phive -Pyarn package \
-pl core,sql/catalyst,sql/core -am \
2>&1 | tail -30
# 3. Open in IntelliJ IDEA
# File → Open → select the spark/ directory
# Wait for Maven import and indexing (5–20 minutes first time)
# Confirm: File → Project Structure → SDKs → Java 11 or 17
# 4. Verify navigation works:
# Press Cmd+O (Mac) or Ctrl+N (Linux/Windows)
# Type: SparkSession
# You should see org.apache.spark.sql.SparkSession
Steps
Step 1 — Map the Module Structure (45 min)
Open spark/pom.xml (the root). Find the <modules> block.
-
List every
<module>entry. There are approximately 30–40. -
For each of the following 8 modules, write one sentence describing its responsibility:
coresql/catalystsql/coresql/hiveresource-managers/yarnnetwork/commonnetwork/shufflecommon/unsafe
-
Draw a simple dependency diagram (ASCII is fine) showing which of these 8 modules depend on which others.
Hint: Open each module's
pom.xmland look at its<dependencies>. The<artifactId>values show you the module names.
Save this in execution-trace.md under ## Module Map.
Step 2 — Trace the Call Path (2–3 hours)
Starting at SparkSession.read(), trace the code path to FileScanRDD.compute(). For each class in the chain, record:
- The module it lives in (e.g.,
sql/core) - The fully qualified class name
- One sentence describing its responsibility
- The specific method where control transfers to the next class in the chain
Use IntelliJ's navigation tools:
Cmd+B/Ctrl+B— Go to DeclarationOpt+F7/Alt+F7— Find UsagesCtrl+H— Type Hierarchy (for abstract classes and interfaces)Cmd+Shift+F/Ctrl+Shift+F— Full-text search across the project
Starting point:
// In IntelliJ: Cmd+O → "SparkSession"
// Find the method: def read: DataFrameReader
val df = spark.read.csv("path/to/file.csv")
Waypoints to find and record (you fill in the details):
1. SparkSession.read() → sql/core
2. DataFrameReader.csv(path) → sql/core
3. DataFrameReader.load() → sql/core
4. DataSource.apply() / DataSource.resolveRelation() → sql/core
5. CSVFileFormat (or TextInputCSVDataSource) → sql/core
6. LogicalRelation → sql/catalyst
7. FileSourceScanExec → sql/core
8. FileScanRDD → sql/core
9. FileScanRDD.compute() → sql/core
When does the physical plan get built? The logical plan is built immediately when you call
csv(). The physical plan is built lazily when you call an action (.show(),.count(),.collect()). Find whereQueryExecution.executedPlanis called — this is the boundary.
Save this as a table in execution-trace.md under ## Execution Trace.
Step 3 — The Logical → Physical Plan Boundary (30 min)
Find and read these two classes:
QueryExecution(insql/core/)SparkPlanner(insql/core/)
Answer these questions in execution-trace.md under ## Plan Boundary:
- What method in
QueryExecutiontriggers physical plan creation? What does it call? - What is
SparkPlanner's role? What does it do with an optimized logical plan? - In 3 sentences or fewer: explain the logical → physical plan transition in your own words, as if explaining it to an engineer who has never seen Spark internals.
Step 4 — Draw the Module Dependency Diagram (30 min)
For the classes you touched in Step 2, draw an ASCII diagram showing:
- Which module each class belongs to
- Which module depends on which (draw an arrow from dependent → dependency)
Example format:
sql/core ──depends on──▶ sql/catalyst
sql/core ──depends on──▶ core
sql/catalyst ──depends on──▶ core
Save this in execution-trace.md under ## Module Dependencies.
Expected Output
lab-01-codebase-spelunking/
├── README.md ← this file
└── execution-trace.md ← your completed trace document
execution-trace.md structure:
## Module Map
[8 module descriptions + dependency diagram]
## Execution Trace
[Table: class name | module | responsibility | handoff method]
## Plan Boundary
[3-question answers + 3-sentence explanation]
## Module Dependencies
[ASCII dependency diagram for classes touched]
Verification
Your trace is correct if:
- Every class in your table is a real class you can open in IntelliJ
- The handoff methods connect correctly (output of one is input to next)
LogicalRelationappears beforeFileSourceScanExecin your list- Your plan boundary explanation correctly identifies that the physical plan is built lazily
Optional verification: set a breakpoint in FileScanRDD.compute() and run a small Spark app locally. Confirm the call stack matches your trace.
Talking Points
- Why is
DataSourcea pivot point? It is the registry that maps a format name ("csv") to itsFileFormatimplementation. This is the extensibility point for custom data sources. - Why is the plan built lazily? Lazy evaluation lets the optimizer see the entire plan before committing. It also means you can build a plan in a driver application and serialize it to executors without running anything.
- What is
InternalRowvsRow?InternalRowis the unboxed binary format used inside the execution engine.Rowis the user-facing boxed API. The conversion happens at the API boundary (e.g.,Dataset.collect()).
Resume Bullet
"Navigated Apache Spark's multi-module Maven codebase from scratch; produced an annotated execution trace from
SparkSession.read().csv()through Catalyst optimization toFileScanRDD.compute(), documenting module boundaries, class responsibilities, and the logical→physical plan transition."