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


##########
core/src/test/scala/org/apache/spark/scheduler/ReplayListenerSuite.scala:
##########
@@ -207,6 +207,56 @@ class ReplayListenerSuite extends SparkFunSuite with 
BeforeAndAfter with LocalSp
     assert(diagnostic.length < 1200)
   }
 
+  test("SPARK-60158: Line buffer range appends match single-character 
appends") {
+    val alphabet = Array('x', '\r', 0x00e9.toChar, 0x4e2d.toChar, 
0xd83d.toChar, 0xde00.toChar)
+    val random = new scala.util.Random(60158)
+    for (_ <- 0 until 2000) {
+      val chars = 
Array.fill(random.nextInt(64))(alphabet(random.nextInt(alphabet.length)))
+      val limit = random.nextInt(80)
+      val single = new ReplayListenerBus.BoundedLineBuffer(limit)
+      chars.foreach(single.append)
+      val ranged = new ReplayListenerBus.BoundedLineBuffer(limit)
+      var start = 0
+      while (start < chars.length) {
+        val end = math.min(chars.length, start + random.nextInt(8))
+        ranged.append(chars, start, end)
+        start = end
+      }
+      withClue(s"chars=${chars.map(_.toInt).mkString(",")} limit=$limit: ") {
+        assert(ranged.result() == single.result())
+        assert(ranged.length == single.length)
+        assert((ranged.capacity == 0) == (single.capacity == 0))
+      }
+    }
+  }
+
+  test("SPARK-60158: Replay splits lines across read buffers like a reference 
splitter") {
+    val alphabet = Seq("x", "\r", 0x00e9.toChar.toString, 
0x4e2d.toChar.toString,
+      new String(Character.toChars(0x1f600)))
+    val random = new scala.util.Random(60158)
+    for (_ <- 0 until 50) {
+      val lines = Seq.fill(random.nextInt(20)) {
+        val length = if (random.nextInt(4) == 0) random.nextInt(20000) else 
random.nextInt(100)
+        Seq.fill(length)(alphabet(random.nextInt(alphabet.length))).mkString
+      }
+      val data = lines.mkString("\n") + (if (random.nextBoolean()) "\n" else 
"")
+      val limit = 1 + random.nextInt(30000)
+      // Reference: split on '\n', drop one trailing CR, keep lines within the 
byte limit.
+      val expected = (if (data.isEmpty) Seq.empty else data.split("\n", 
-1).toSeq)
+        .dropRight(if (data.endsWith("\n")) 1 else 0)
+        .map(line => if (line.endsWith("\r")) line.dropRight(1) else line)
+        .filter(_.getBytes(StandardCharsets.UTF_8).length <= limit)
+      val seen = new ArrayBuffer[String]
+      assert(new ReplayListenerBus(limit).replay(
+        new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), 
"reference",

Review Comment:
   Added in 714c458a1f4. The reference splitter test now also runs each input 
through a stream that returns 1 to N bytes per `read` (N is 1, 3, 64 or 8192) 
and reports `available() == 0`, so chunks end at arbitrary points, including 
right after a `\r` and right before or at a `\n`. A mutation that does not 
treat a `\n` at the start of a new chunk as a line end passes all the existing 
tests but fails this one.
   



##########
core/src/main/scala/org/apache/spark/scheduler/ReplayListenerBus.scala:
##########
@@ -275,19 +294,44 @@ private[spark] object ReplayListenerBus {
 
     def append(c: Char): Unit = {

Review Comment:
   Done in 714c458a1f4, together with [3]. `append(c)` and the range `append` 
are replaced by a single `appendUntilNewline(chars, start, end)`, which is what 
`fetchLine` calls, so there is one copy of the limit and release logic. The 
SPARK-59804 buffer test now uses it, and the equivalence test is replaced by 
one that splits random lines at random points and compares the result with a 
reference (drop one trailing CR, keep the line if its UTF-8 length fits), and 
checks that the buffer is released once the line exceeds the limit plus the CR 
allowance.
   



##########
core/src/main/scala/org/apache/spark/scheduler/ReplayListenerBus.scala:
##########
@@ -116,15 +130,20 @@ private[spark] class ReplayListenerBus(
 
       @tailrec private def fetchLine(): (String, Int) = {
         val buffer = new BoundedLineBuffer(effectiveMaxLineLength)
-        var c = reader.read()
-        if (c == -1) {
+        if (!fill()) {
           null
         } else {
           val index = lineIndex
           lineIndex += 1
-          while (c != -1 && c != '\n') {
-            buffer.append(c.toChar)
-            c = reader.read()
+          var endOfLine = false
+          while (!endOfLine && fill()) {
+            var i = pos
+            while (i < len && chars(i) != '\n') {

Review Comment:
   Changed in 714c458a1f4: `appendUntilNewline` finds the `\n` and adds up the 
UTF-8 width in the same loop, and once the line goes over the limit it only 
searches for `\n`. The full-replay numbers stay the same (lz4 0.22 s, zstd 0.19 
s, vs 0.20 s / 0.17 s for `Source.getLines()`).
   



##########
core/src/main/scala/org/apache/spark/scheduler/ReplayListenerBus.scala:
##########
@@ -116,15 +130,20 @@ private[spark] class ReplayListenerBus(
 
       @tailrec private def fetchLine(): (String, Int) = {
         val buffer = new BoundedLineBuffer(effectiveMaxLineLength)

Review Comment:
   I'd prefer to leave this out. The numbers are already on par with 
`Source.getLines()`, and the fast path would need its own handling of the 
trailing CR and the limit, which would be a second place to keep in sync with 
`BoundedLineBuffer`.
   



##########
core/src/main/scala/org/apache/spark/scheduler/ReplayListenerBus.scala:
##########
@@ -89,13 +89,27 @@ private[spark] class ReplayListenerBus(
     val decoder = StandardCharsets.UTF_8.newDecoder()
       .onMalformedInput(CodingErrorAction.REPORT)
       .onUnmappableCharacter(CodingErrorAction.REPORT)
-    val reader = new BufferedReader(new InputStreamReader(logData, decoder))
+    // Read decoded characters in bulk and scan them for '\n', instead of 
calling read() per
+    // character on a BufferedReader, which takes a lock on every call.
+    val reader = new InputStreamReader(logData, decoder)

Review Comment:
   Thanks. I'd keep decoding every line for now: SPARK-59804 kept failing the 
replay on malformed UTF-8 inside a skipped line (`Replay still rejects 
malformed UTF-8 in skipped lines`), and splitting on raw bytes would drop that. 
Raw-byte splitting also comes up for an incomplete UTF-8 sequence at EOF 
(SPARK-60114), so I'll look at it there if the remaining overhead matters.
   



##########
core/src/test/scala/org/apache/spark/scheduler/ReplayListenerSuite.scala:
##########
@@ -207,6 +207,56 @@ class ReplayListenerSuite extends SparkFunSuite with 
BeforeAndAfter with LocalSp
     assert(diagnostic.length < 1200)
   }
 
+  test("SPARK-60158: Line buffer range appends match single-character 
appends") {
+    val alphabet = Array('x', '\r', 0x00e9.toChar, 0x4e2d.toChar, 
0xd83d.toChar, 0xde00.toChar)
+    val random = new scala.util.Random(60158)
+    for (_ <- 0 until 2000) {
+      val chars = 
Array.fill(random.nextInt(64))(alphabet(random.nextInt(alphabet.length)))
+      val limit = random.nextInt(80)
+      val single = new ReplayListenerBus.BoundedLineBuffer(limit)
+      chars.foreach(single.append)
+      val ranged = new ReplayListenerBus.BoundedLineBuffer(limit)
+      var start = 0
+      while (start < chars.length) {
+        val end = math.min(chars.length, start + random.nextInt(8))
+        ranged.append(chars, start, end)
+        start = end
+      }
+      withClue(s"chars=${chars.map(_.toInt).mkString(",")} limit=$limit: ") {
+        assert(ranged.result() == single.result())
+        assert(ranged.length == single.length)
+        assert((ranged.capacity == 0) == (single.capacity == 0))
+      }
+    }
+  }
+
+  test("SPARK-60158: Replay splits lines across read buffers like a reference 
splitter") {

Review Comment:
   Added `SPARK-60158: Replay reports physical line numbers for lines spanning 
read buffers` in 714c458a1f4: a skipped 20000-char line followed by a malformed 
line, with full reads and with 1-byte and up-to-100-byte reads. It checks 
`first skipped line: 2` and `Malformed line #4`, and fails if the line index is 
advanced per chunk instead of per line.
   



-- 
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