Appearance
Caching and memory widgets
The board widgets for caching opportunities, memory utilization and cache storage.
Caching Opportunities (tag
CACHE,CachingOpportunity.tsx): app-level SQL relation-reuse detector (DETECTORS entrycachingOpportunity), computed fromctx.sql. It walks each execution'splanTreeand keys every scan by its stable pre-AQE identity viascanRelationId(plan-summary.ts):<parquet|orc|csv|json>:<db.table>,delta:<db.table>,jdbc:<schema.table>. That dedupes relations within one execution (self-joins count once) and flags any relation scanned by>= minExecutions(2) distinct executions. Relation identity comes from the catalog-qualified scan name (nodeName, e.g.Scan parquet spark_catalog.db.t), not theLocation:path, since Spark truncates that path at ~100 chars and points it at_delta_logfor Delta tables; internal Delta-log metadata scans are dropped. Read bytes come from the scan'ssize of files readmetric (0/unknown for JDBC). Findings carryexecutionReuse(value= distinct-execution count),relation,format,executionIds,totalReadBytes, and a size-awarerecommendation(confidencescaledlow/medium/highviacachingReuseConfidenceoff reuse-execution count, plusvalidationRequired). Renders nothing when clean (no card in the DOM). One row per relation: name + format badge, reuse count,Data read(formatBytes, em-dash when unknown), sorted bytotalReadBytesthen reuse count descending; the card states the fix once (fixFor), not per row. Rows are pagedVISIBLE_LIMIT(6) at a time (usePagedRows+RowPagination), and a route to a row jumps to its page. Pure-RDD-API apps (no SQL executions) produce no finding: a deliberate trade-off, since an RDD-lineage heuristic surfaces only internal query-engine RDDs on DataFrame/SQL workloads.It also detects composite reuse. When the same join/union subtree (not just a leaf scan) recurs across
>= minExecutionsdistinct SQL executions, the detector emits onevariant:'composite'finding recommending caching the derived join/union result instead of two independent leaf-relation rows, and suppresses the leaf findings for relations fully covered by it. A relation reused beyond the composite's executions keeps a residual leaf finding for just the uncovered executions. Composite identity is structural: an anchor plan-shape fingerprint built byfindCompositeCandidates(packages/core/src/detectors.ts; the same shapecomputePlanShapesproduces withopts.includeDetail, computed inline in one bottom-up pass) folds the join/union node's own normalizeddetail(join type, columns, literals, with expr ids,plan_id=, codegen-stage numbers, and AQE's BuildLeft/BuildRight stripped, and commutative equality operands canonicalized) plus its children's detail-free shapes. Descendant nodes never contribute detail text, only structural shape, so a shared join reused across differently-filtered pre-join scans still matches (a deliberate trade-off) while a different join condition on the same tables does not collide. Nested composites (an inner join reused both standalone and inside an outer join) dedupe one level at a time: an inner composite fully covered by a qualifying outer composite's execution set is suppressed entirely, and a superset recomputes a residual finding over just its extra executions. Reuse via pure projection/aggregation with no join/union underneath stays leaf-level, not modeled as composite.variant:'composite'findings carryoperator('join'|'union'),relations(leaf relations under the composite, for display), andformat:'derived'(a sentinel: composites have no scan storage format). They render aJOIN/UNIONoperator badge (styled distinct from the format badges) plus a confidence marker (confidencescaled viacachingReuseConfidence,validationRequired) inCachingOpportunity.tsx.Memory Utilization (tag
MEM): app-level card combining three sub-findings from thememoryUtilizationDETECTORS entry (packages/core/src/detectors.ts): idle-cores rate (busy-core-time from the worker's run-aggregates sweep vs. the run's allocated core time,metrics.allocation.coreHours), one heap band (the highest executor heap peak vs. allocated, fromexecutorHeapPeaksinpackages/core/src/executor-peaks.ts: a distinctdataUnavailablefinding renders when the log has no non-zero peak), and an unverified memory-waste model (confidencescaleslow/medium/highviamemoryWasteConfidence, off how far the wasted/used ratio sits past the 1.5× buffer). There is no driver-memory band: the worker only extracts allocatedspark.driver.memory, never a driver actual-usage metric, so there is nothing to band against. The separateutilizationDETECTORS entry (anavgUtilizationinfo finding: busy share of allocated core time below 60%) has its own card, Executor Utilization (tagUTIL,ExecutorUtilization.tsx): areference-region widget in its own right that renders only with an active finding.Cache Storage (tag
CSTOR): app-level card driven by thecacheUtilizationDETECTORS entry (packages/core/src/detectors.ts), evaluating two per-RDD proxies overctx.app.rddInfosince Spark event logs carry no runtime block-access/read-count data.rddInfo's cache figures come fromSparkListenerBlockUpdated(recordBlockUpdateinevent-handlers.ts, only written withspark.eventLog.logBlockUpdates.enabled=true): each RDD's peak count of resident partitions, with the memory/disk bytes at the latest moment that peak held, so anunpersist()before the log ends doesn't erase it. A block's bytes count only where its storage level says it lives (as in Spark'sAppStatusListener): a drop from memory to disk still reports the dropped bytes asMemory Size. A removed executor's blocks are dropped with it (Spark logs no update for them), and once an RDD has block updates a later stage's RDD Info can't reset its storage level toNONEafter anunpersist(). The corpuscache-memory-onlyandcache-memory-and-disklogs exercise both rules. Without block updates they fall back toSparkListenerStageSubmitted's RDD Info (storageSourcerecords which), which is always 0 since Spark 2.3; Spark 1.x fills it only onStageCompleted, which isn't read. The two proxies are partial caching (numCachedPartitions / numPartitions < 0.90,< 0.50for the warning tier) and disk spillover forMEMORY_AND_DISK*RDDs (diskSize / (memorySize + diskSize) > 0.15,> 0.40for the warning tier;DISK_ONLYRDDs are never flagged).confidencescaleslow/medium/highviacacheSampleConfidence(rdd.numPartitions), because the ratio is a storage snapshot, not a runtime read-count, and more partitions average that snapshot noise into a more stable ratio. When persisted RDDs have no storage evidence at all (no block updates, block-update logging not enabled in the app config, a recorded Spark version of 2.3 or later, and every RDD Info figure 0), the detector emits onestorageUnobservedcaveat (dataUnavailable: true,info) namingspark.eventLog.logBlockUpdates.enabled. UnlikememoryUtilization's caveat it counts forisRealFinding, so the card still mounts (with the caveat and no table) and Cache Storage never lands in Clean checks for a run that couldn't be checked;isEligiblekeeps it out of Fix these first. The RDD table renders unconditionally; flagged rows get an inlineCSTORtag next to the RDD name, and every flagged RDD's recommendation renders below the table, worst-first.