Skip to content

fix: #158: ClassCastException in ComponentReportQuery.addZeroLengthArrays#165

Open
jasonk000 wants to merge 1 commit intomasterfrom
jkoch/issue-158
Open

fix: #158: ClassCastException in ComponentReportQuery.addZeroLengthArrays#165
jasonk000 wants to merge 1 commit intomasterfrom
jkoch/issue-158

Conversation

@jasonk000
Copy link
Contributor

Should fix #158.

The addZeroLengthArrays method was reading the wrong column indices from the arrays_grouped_by_size refined table, causing a ClassCastException ("Long cannot be cast to Integer") whenever the retained size of zero-length arrays of a single type exceeded 5% of the component's total retained size.

Root cause

Both addZeroLengthArrays and the similar addEmptyCollections method read column values from a Quantize-backed RefinedTable. The collections query (collections_grouped_by_size) groups by a single key - the collection size - so its column layout is:

col 0: collection size (Integer, key)
col 1: count (Integer, Quantize.COUNT)
col 2: shallow heap (Bytes)
col 3: retained size (Long, derived)

The array query (arrays_grouped_by_size) was changed in commit 185b069 (Bug 573258, April 2021) to group by TWO keys - array length AND single-instance size - in order to distinguish, for example, byte[0] from int[0] which have the same length but different heap footprints. That shifted every non-key column right by one:

col 0: array length (Integer, key)
col 1: single-instance size (Long, key) <- new key column
col 2: count (Integer, Quantize.COUNT)
col 3: shallow heap (Bytes)
col 4: retained size (Long, derived)

addZeroLengthArrays was introduced in the same commit and was written with the two-key layout for the retained-size read (col 4, correct), but used the single-key layout for the count (col 1, wrong) and the formatted retained-size string (col 3, wrong - that is shallow heap).

Why it went unnoticed

The buggy casts are guarded by two conditions that are rarely true simultaneously:

  1. The most-frequently-seen array length for a given class is 0 (i.e. zero-length instances dominate that class by shallow heap).
  2. The approximate retained size of those zero-length instances exceeds totalSize / 20 (5% of the component's retained heap).

Standard heap dumps from typical applications almost never satisfy condition 2. It requires a pathological allocation pattern where zero-length arrays of a single type collectively retain a large fraction of the heap.

Fix

  • col 1 -> col 2 for numberOfObjects (correct Quantize.COUNT column)
  • col 3 -> col 4 for retainedSize string (correct derived-data column, also ensures the RetainedSizeFormat "~" approximation prefix is applied rather than showing the shallow heap value)

Add testArraysBySizeColumnTypes to QueriesTest to pin the column contract of arrays_grouped_by_size. The test runs on the existing byte[] population of the standard test snapshot and will fail immediately if the key-column count of ArraysBySizeQuery changes without a corresponding update to ComponentReportQuery.

…rays

The addZeroLengthArrays method was reading the wrong column indices from
the arrays_grouped_by_size refined table, causing a ClassCastException
("Long cannot be cast to Integer") whenever the retained size of
zero-length arrays of a single type exceeded 5% of the component's
total retained size.

Root cause
----------
Both addZeroLengthArrays and the similar addEmptyCollections method
read column values from a Quantize-backed RefinedTable. The collections
query (collections_grouped_by_size) groups by a single key - the
collection size - so its column layout is:

  col 0: collection size (Integer, key)
  col 1: count           (Integer, Quantize.COUNT)
  col 2: shallow heap    (Bytes)
  col 3: retained size   (Long, derived)

The array query (arrays_grouped_by_size) was changed in commit 185b069
(Bug 573258, April 2021) to group by TWO keys - array length AND
single-instance size - in order to distinguish, for example, byte[0]
from int[0] which have the same length but different heap footprints.
That shifted every non-key column right by one:

  col 0: array length          (Integer, key)
  col 1: single-instance size  (Long, key)     <- new key column
  col 2: count                 (Integer, Quantize.COUNT)
  col 3: shallow heap          (Bytes)
  col 4: retained size         (Long, derived)

addZeroLengthArrays was introduced in the same commit and was written
with the two-key layout for the retained-size read (col 4, correct),
but used the single-key layout for the count (col 1, wrong) and the
formatted retained-size string (col 3, wrong - that is shallow heap).

Why it went unnoticed for four years
-------------------------------------
The buggy casts are guarded by two conditions that are rarely true
simultaneously:

  1. The most-frequently-seen array length for a given class is 0
     (i.e. zero-length instances dominate that class by shallow heap).
  2. The approximate retained size of those zero-length instances
     exceeds totalSize / 20 (5% of the component's retained heap).

Standard heap dumps from typical applications almost never satisfy
condition 2. It requires a pathological allocation pattern where
zero-length arrays of a single type collectively retain a large
fraction of the heap.

Fix
---
- col 1 -> col 2 for numberOfObjects (correct Quantize.COUNT column)
- col 3 -> col 4 for retainedSize string (correct derived-data column,
  also ensures the RetainedSizeFormat "~" approximation prefix is
  applied rather than showing the shallow heap value)

Add testArraysBySizeColumnTypes to QueriesTest to pin the column
contract of arrays_grouped_by_size. The test runs on the existing
byte[] population of the standard test snapshot and will fail
immediately if the key-column count of ArraysBySizeQuery changes
without a corresponding update to ComponentReportQuery.
@jasonk000 jasonk000 requested review from kgibm and krumts March 2, 2026 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

top_components report error: class java.lang.Long cannot be cast to class java.lang.Integer

1 participant