รีวิวจริงที่ 2: CLI ประมวลผลไฟล์
สคริปต์สไตล์ CLI เล็ก ๆ: อ่านไฟล์ข้อความ, แปลงทุกบรรทัดเป็นตัวพิมพ์ใหญ่, เขียนผลลัพธ์ลงอีกไฟล์หนึ่ง — ตัวอย่างแบบ “streams” ที่ AI assistant เลือกใช้เป็นประจำ มันเขียนไฟล์ input ของตัวเองเพื่อให้ demo สมบูรณ์ในตัว และเก็บกวาดทั้งสองไฟล์ตอนจบ bug สี่ตัวจาก AI bug catalog ถูกปลูกไว้ในโค้ดประมาณ 35 บรรทัด
โปรแกรม
หัวข้อที่มีชื่อว่า “โปรแกรม”Output (verified on Node 24)
read 43 bytes synchronously before streaming even startsมีแค่ stdout บรรทัดเดียว แล้ว crash — dest.on('finish', ...) ไม่เคยทำงาน ดังนั้นบรรทัด output: จึงไม่เคยโผล่มาเลย
ผลลัพธ์จริง (stderr ตามหลังบรรทัดข้างบน):
node:events:486 throw er; // Unhandled 'error' event ^Error: corrupt row encountered at Transform.transform [as _transform] (...)Emitted 'error' event on Transform instance at: at Transform.onerror (...)ลองหา bug ก่อนอ่านต่อ
หัวข้อที่มีชื่อว่า “ลองหา bug ก่อนอ่านต่อ”bug ที่ปลูกไว้สี่ตัว — หนึ่งเรื่องการใช้ memory, หนึ่งอายุของทรัพยากร, หนึ่งขอบเขตข้อมูล, หนึ่ง call ที่บล็อก ลองเดาทั้งสี่ตัวก่อนเปิดคำตอบด้านล่าง
Bug 1 — fs.readFileSync(inputPath) ก่อน stream ไฟล์เดียวกันนั้นทันที
อาการ: จุดประสงค์ทั้งหมดของ stream คือหลีกเลี่ยงการเก็บทั้งไฟล์ไว้ใน memory พร้อมกัน — บรรทัดนี้ทำลายจุดประสงค์นั้นทันที โดยโหลดทั้งไฟล์แบบ synchronous ก่อนที่แม้แต่ chunk เดียวจาก stream จะถูกอ่าน
หลักฐาน: บนไฟล์ demo เล็ก ๆ นี้มันไม่มีต้นทุนที่เห็นได้ แต่บนไฟล์ log ขนาดหลายกิกะไบต์ มันคือความต่างระหว่างการใช้ memory คงที่กับการ crash แบบ out-of-memory “43 bytes” ในผลลัพธ์จริงข้างบนดูเล็กจนหลอกได้ — ต้นทุนของ bug นี้แปรผันตามขนาดไฟล์ ไม่ใช่ว่ามี bug นี้อยู่หรือไม่
วิธีแก้: ลบบรรทัดนี้ทิ้งไปเลย ไม่มีอะไรข้างล่างในไฟล์นี้ใช้ wholeFile เลย — มันมีอยู่แค่เพื่อ diagnostic ที่ไม่เคยถูกลบออก
Bug 2 — source.pipe(upper).pipe(dest); ที่ไม่มีการจัดการ error
อาการ: .pipe() forward ข้อมูลระหว่าง stream แต่ไม่เคย forward event 'error' — error บน stream ใดใน chain ก็ไม่มีที่ไป
หลักฐาน: crash จริงข้างบน callback(new Error(...)) ของ Transform บนแถวที่เสียยกระดับเป็น event 'error' บน upper; เมื่อไม่มีอะไรฟังมันเลย Node ถือว่ามันร้ายแรงและฆ่าทั้งโปรเซสก่อนที่ dest จะเสร็จด้วยซ้ำ
วิธีแก้: stream.pipeline() จาก node:stream/promises ซึ่งส่ง error ของ stream ใดก็ตามมารวมที่จุดเดียว และยัง destroy ทุก stream ใน chain ด้วยเพื่อไม่ให้มีอะไรค้างครึ่ง ๆ กลาง ๆ
Bug 3 — chunk.toString('utf8') ข้างในตัว transform
อาการ: แต่ละ chunk ถูก decode เป็น string แยกกัน โดย highWaterMark: 8 ถูกตั้งให้เล็กโดยเจตนาเพื่อตัดข้อความไทยแบบ multi-byte คาบกลางตัวอักษร
หลักฐาน: สิ่งนี้มองไม่เห็นใน crash ข้างบน (แถวที่เสีย crash โปรเซสก่อนที่ output ของ bug นี้จะโผล่มา) แต่การรันแค่สองบรรทัดแรกของ input (ไม่มีแถวที่เสีย) จะทำให้ข้อความไทยที่เสียเห็นได้ตรง ๆ ในไฟล์ output — การเสียแบบเดียวกับอักขระ replacement � เหมือน pattern 15 ใน AI bug catalog
วิธีแก้: StringDecoder จาก node:string_decoder ซึ่งบัฟเฟอร์ลำดับ multi-byte ที่ยังไม่สมบูรณ์ไว้จนกว่า chunk ที่ทำให้มันสมบูรณ์จะมาถึง
Bug 4 — เรียก slowHash(text) แบบ synchronous ข้างใน transform()
อาการ: loop ที่หนัก CPU ซ้อนกันรันแบบ synchronous สำหรับทุก chunk บล็อก event loop ตลอดระยะเวลาทั้งหมดของมัน — ข้างในโค้ดที่ควรจะ “streaming” เพื่อหลีกเลี่ยงการบล็อกโดยเฉพาะ
หลักฐาน: มองไม่เห็นใน output ของ demo เล็ก ๆ นี้ (ผลลัพธ์ของมัน คือ sum ไม่ได้ถูกใช้ด้วยซ้ำ) ซึ่งนั่นแหละคืออันตราย: มันเสียเวลาจริงที่วัดได้ภายใต้ load โดยไม่เคยให้คำตอบที่ผิดเห็นได้ชัดเลย
วิธีแก้: ลบมันทิ้ง — ไม่มีอะไรในสคริปต์นี้ที่ต้องการ checksum จริง ๆ ถ้าการคำนวณต่อ chunk จริงจำเป็นต้องมี มันควรอยู่นอก main thread (worker_thread) แทนที่จะบล็อก request หรือ timer อื่นทุกตัวขณะที่มันทำงาน
เวอร์ชันที่แก้แล้ว
หัวข้อที่มีชื่อว่า “เวอร์ชันที่แก้แล้ว”Output (verified on Node 24)
output: "HELLO\nสวัสดี\nWORLD\n"บรรทัดภาษาไทยรอดจากการไปกลับครั้งนี้ครบถ้วน — StringDecoder เก็บ byte ที่ถูกตัดจาก chunk เล็กแรกไว้จนกว่า chunk ถัดไปจะทำให้ตัวอักษรสมบูรณ์ แทนที่จะ decode แต่ละครึ่งที่ไม่สมบูรณ์แยกกัน ถ้า run นี้มีบรรทัด CORRUPT_ROW เหมือนเวอร์ชันที่มี bug ด้วย catch block ของ pipeline() ก็จะ print pipeline failed: corrupt row encountered แล้วจบแบบสะอาดที่ exit code 0 แทนที่จะ crash โปรเซสแบบที่ chain .pipe() ที่ไม่มีการจัดการ error ของเวอร์ชันที่มี bug ทำ
เครื่องมือจับอะไรได้บ้าง
หัวข้อที่มีชื่อว่า “เครื่องมือจับอะไรได้บ้าง”npx eslint บนเวอร์ชันที่มี bug: ไม่มีปัญหาเลย ไม่มี bug ทั้งสี่ตัวนี้เลย — การ readFileSync ล่วงหน้าที่ไม่จำเป็น, chain .pipe() ที่ไม่มีการจัดการ error, toString() ต่อ chunk, และ loop แบบ synchronous ข้างใน stream callback — ตรงกับกฎไหนใน eslint-plugin-n หรือ eslint-plugin-promise เลย
node --trace-warnings บนเวอร์ชันที่มี bug: ไม่มีอะไรเลย — ตัวนี้ crash ผ่าน event 'error' ที่ไม่ถูกจัดการบน stream ซึ่งเป็นกลไกที่ต่างจาก event warning ที่ flag นั้นถูกสร้างมาเพื่อ trace
การรันไฟล์จับ bug 2 ได้ทันที (การ crash) และทางอ้อมก็จับ bug 1 ได้ด้วย (การอ่านแบบ synchronous ที่รันก่อนหน้ามันทันที เห็นได้ในบรรทัดเดียวของผลลัพธ์จริง) bug 3 และ 4 ไม่มีอาการที่เห็นได้เลยใน run นี้ — bug 3 ต้องการ run ที่ไม่ crash เพื่อเห็น output ที่เสีย และ bug 4 ต้องการ input ที่ใหญ่กว่ามากหรือ profiler (--cpu-prof จากบท Verification Tools) เพื่อให้เห็นเป็น latency ที่วัดได้