viirya commented on code in PR #60196:
URL: https://github.com/apache/spark/pull/60196#discussion_r4500475750


##########
core/src/main/java/org/apache/spark/unsafe/map/BytesToBytesMap.java:
##########
@@ -175,6 +175,12 @@ private enum MapState {
    */
   private final long pageSizeBytes;
 
+  /**
+   * The size of the next data page to allocate. It starts at the initial page 
size and doubles
+   * with every new page until it reaches {@link #pageSizeBytes}.
+   */
+  private long nextPageSizeBytes;

Review Comment:
   `reset()` doesn't restore `nextPageSizeBytes`, so a map created with a small 
initial page goes straight back to full-size pages after a reset, while the 
javadoc says "Resets this map to initialized state". Only aggregation calls 
`reset()` today, and there the two sizes are equal, so nothing is broken now. 
Should we keep a final `initialPageSizeBytes` field and restore it here so the 
new constructor's contract also holds after a reset?



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/joins/HashedRelationSuite.scala:
##########
@@ -141,6 +141,60 @@ abstract class HashedRelationSuite extends 
SharedSparkSession {
     assert(java.util.Arrays.equals(os2.toByteArray, os.toByteArray))
   }
 
+  test("SPARK-61082: many UnsafeHashedRelations fit in one task when each 
holds little data") {
+    // UnsafeHashedRelation takes its page size from SparkEnv (about 64 MiB in 
tests), not from this
+    // memory manager. 16 relations rounded up to a full page each would need 
about 1 GiB, 4x the
+    // memory available.
+    val smallUmm = newMemoryManager(ByteUnit.MiB.toBytes(64), 
ByteUnit.MiB.toBytes(256))

Review Comment:
   `UnsafeHashedRelation` takes its page size from `SparkEnv`, so the 64 MiB 
passed to `newMemoryManager` has no effect here. Whether this test fails 
without the fix then depends on the test JVM's heap: with a 2g heap the derived 
page size can be 16 MiB, and 16 relations would roughly fit in 256 MiB anyway. 
Could we assert the precondition, e.g. 
`assert(SparkEnv.get.memoryManager.pageSizeBytes >= ByteUnit.MiB.toBytes(32))`, 
so the test can't silently stop covering the bug?



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/joins/HashedRelationSuite.scala:
##########
@@ -141,6 +141,60 @@ abstract class HashedRelationSuite extends 
SharedSparkSession {
     assert(java.util.Arrays.equals(os2.toByteArray, os.toByteArray))
   }
 
+  test("SPARK-61082: many UnsafeHashedRelations fit in one task when each 
holds little data") {
+    // UnsafeHashedRelation takes its page size from SparkEnv (about 64 MiB in 
tests), not from this
+    // memory manager. 16 relations rounded up to a full page each would need 
about 1 GiB, 4x the
+    // memory available.
+    val smallUmm = newMemoryManager(ByteUnit.MiB.toBytes(64), 
ByteUnit.MiB.toBytes(256))
+    val smallMm = new TaskMemoryManager(smallUmm, 0)
+    val buildKey = Seq(BoundReference(0, StringType, false))
+    val toUnsafeRow = UnsafeProjection.create(Array[DataType](StringType, 
IntegerType))
+    val toUnsafeKey = UnsafeProjection.create(Array[DataType](StringType))
+    val relations = ArrayBuffer.empty[HashedRelation]
+    try {
+      (0 until 16).foreach { i =>
+        val rows = (0 until 10).map { k =>
+          toUnsafeRow(InternalRow(UTF8String.fromString(s"key$k"), k + 
i)).copy()
+        }
+        val relation = HashedRelation(rows.iterator, buildKey, 
taskMemoryManager = smallMm)
+        assert(relation.isInstanceOf[UnsafeHashedRelation])
+        relations += relation
+      }
+      relations.zipWithIndex.foreach { case (relation, i) =>
+        assert(relation.estimatedSize < 2 * 
UnsafeHashedRelation.INITIAL_PAGE_SIZE_BYTES)
+        (0 until 10).foreach { k =>
+          val key = toUnsafeKey(InternalRow(UTF8String.fromString(s"key$k")))
+          assert(relation.getValue(key).getInt(1) === k + i)
+        }
+      }
+    } catch {
+      // SparkOutOfMemoryError is fatal to ScalaTest and would abort the whole 
suite.
+      case e: SparkOutOfMemoryError =>
+        fail("Could not build 16 small UnsafeHashedRelations in one task", e)
+    } finally {
+      relations.foreach(_.close())
+    }
+    assert(smallUmm.executionMemoryUsed === 0)
+  }
+
+  test("SPARK-61082: UnsafeHashedRelation does not round its page up to the 
page size") {
+    // The single string column is also the key, so a row is its own key.
+    val buildKey = Seq(BoundReference(0, StringType, false))
+    val toUnsafeRow = UnsafeProjection.create(Array[DataType](StringType))
+    val row = toUnsafeRow(InternalRow(UTF8String.fromString("a"))).copy()
+    val relation = HashedRelation(Iterator(row), buildKey, taskMemoryManager = 
mm)
+    val ser = sparkContext.env.serializer.newInstance()

Review Comment:
   nit: the SPARK-60998 deserialization test covers both Java and Kryo. Kryo 
goes through the same `read` helper here, but checking both serializers would 
match that test and protect the Kryo entry point.



##########
core/src/test/java/org/apache/spark/unsafe/map/AbstractBytesToBytesMapSuite.java:
##########
@@ -817,6 +819,31 @@ public void testPeakMemoryUsed() {
     }
   }
 
+  @Test
+  public void dataPagesGrowFromInitialPageSize() {
+    final BytesToBytesMap map = new BytesToBytesMap(taskMemoryManager, 1, 
1024, 4096);
+    try {
+      // All records share one key, so the pointer array does not grow and 
every increase of the
+      // memory consumption is a new data page.
+      final long[] key = new long[]{0};
+      final List<Long> pageSizes = new ArrayList<>();
+      long previousMemory = map.getTotalMemoryConsumption();
+      while (pageSizes.size() < 5) {
+        assertTrue(map.lookup(key, Platform.LONG_ARRAY_OFFSET, 8)
+          .append(key, Platform.LONG_ARRAY_OFFSET, 8, key, 
Platform.LONG_ARRAY_OFFSET, 8));
+        final long memory = map.getTotalMemoryConsumption();
+        if (memory > previousMemory) {
+          pageSizes.add(memory - previousMemory);
+        }
+        previousMemory = memory;
+      }
+      assertEquals(Arrays.asList(1024L, 2048L, 4096L, 4096L, 4096L), 
pageSizes);

Review Comment:
   The javadoc also says a record larger than the next page gets a page of its 
own size. That case is new on this path. Could you add a record bigger than the 
1 KiB initial page here and check its page size?



##########
core/src/main/java/org/apache/spark/unsafe/map/BytesToBytesMap.java:
##########
@@ -259,10 +285,36 @@ public BytesToBytesMap(
       throw new IllegalArgumentException("Page size " + pageSizeBytes + " 
cannot exceed " +
         TaskMemoryManager.MAXIMUM_PAGE_SIZE_BYTES);
     }
+    if (initialPageSizeBytes <= 0) {
+      throw new IllegalArgumentException("Initial page size must be greater 
than 0");
+    }
+    this.nextPageSizeBytes = Math.min(initialPageSizeBytes, pageSizeBytes);
     this.initialCapacity = initialCapacity;
     allocate(initialCapacity);
   }
 
+  /**
+   * Creates a map whose first data page has `initialPageSizeBytes` (at most 
`pageSizeBytes`), and
+   * whose later pages double in size until they reach `pageSizeBytes`. A map 
holding little data
+   * then does not reserve a full page. A record larger than the next page 
gets a page of its size.
+   */
+  public BytesToBytesMap(
+      TaskMemoryManager taskMemoryManager,
+      int initialCapacity,
+      long initialPageSizeBytes,

Review Comment:
   In the 3-arg overload the third argument is `pageSizeBytes`, but in this one 
it is `initialPageSizeBytes`. Since the two `long`s are adjacent, it's easy to 
swap them by mistake. Would `(taskMemoryManager, initialCapacity, 
pageSizeBytes, initialPageSizeBytes)` be safer?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to