Day 15

Phase 1 Finale: Audit Report & Benchmarking

Quiz: 0/160 pts
Phase 1 Capstone Deliverable: Part 2

Audit Report Generation, testing.B & Zero-Alloc Verification

เปลี่ยน Core Parsing Engine จาก Day 14 ให้กลายเป็น CLI Financial Ledger Auditor ที่สมบูรณ์แบบ: สรุปผลตัวเลขทางการเงินแบบ Zero-Heap Allocation, เขียนชุดทดสอบสมรรถนะระดับไมโครวินาทีด้วยเครื่องมือมาตรฐาน testing.B, และพิสูจน์ผลลัพธ์ 0 B/op และ 0 allocs/op บนฮาร์ดแวร์จริง

Section 0: The "Why" & Core Vocabulary

ในโลกของ JVM เมื่อเราต้องการวัดประสิทธิภาพระดับไมโครวินาทีของคำสั่งหักเงินหรืออ่านไฟล์ เราจำเป็นต้องพึ่งพา Framework ภายนอกขนาดใหญ่อย่าง JMH (Java Microbenchmark Harness) ซึ่งต้องติดตั้ง Annotation Processor, รันกระบวนการ Warmup หลายรอบเพื่อรอให้ JIT Compiler ทำการ C2 Tiered Compilation, และมักเกิดปัญหาเรื่อง GC Pause แอบแฝงระหว่างรัน Benchmark

The JMH & JIT Trap We Are Leaving Behind

ใน JVM โค้ดที่รันเร็วใน JMH อาจไม่ได้รันเร็วใน Production เพราะ JIT Compiler ทำการ Dynamic De-optimization เมื่อพบ Branch ใหม่ หรือเมื่อ JVM Heap เริ่มเต็มจนต้องเกิด Major GC Collection

ในทางตรงกันข้าม ภาษา Go มาพร้อมกับเครื่องมือวัดผลสมรรถนะในตัว (testing.B) ที่คอมไพล์เป็น Static Machine Code โดยตรง จึงวัดค่า Latency, Memory Footprint, และอัตรา Throughput ได้ตรงตามพฤติกรรมฮาร์ดแวร์จริงแบบ 100% ปราศจากความผันผวนของ JIT

คำศัพท์สถาปัตยกรรม (Go) นิยามและพฤติกรรมระดับ Runtime / Hardware แนวคิดเทียบเท่าใน Kotlin / JVM
testing.B Struct ใน Standard Library สำหรับควบคุมการรัน Microbenchmark โดย Runtime จะปรับขนาด b.N แบบไดนามิกจนกว่าจะได้ผลลัพธ์ที่นิ่งทางสถิติ JMH @Benchmark และ Blackhole
b.ReportAllocs() คำสั่งสั่งให้ Go Benchmark Harness บันทึกตัวเลข Heap Allocations ต่อรอบการทำงาน (B/op และ allocs/op) JMH Profiler -prof gc
Dead-Code Elimination การที่คอมไพเลอร์ตัดคำสั่งที่ไม่ส่งผลต่อ Output ของฟังก์ชันทิ้งไป ต้องป้องกันด้วยการส่งผลลัพธ์เข้าสู่ Global Package Sink Variable JMH Blackhole.consume()
Audit Aggregation การคำนวณยอดเงินรวม (Net Sum), สถิติรายการที่ถูกต้อง/ผิดพลาด โดยใช้วิธี Accumulate ค่าบน Struct เดียวโดยไม่สร้าง Object บน Heap Java Streams .reduce() / Collectors.summarizingLong()

Benchmark Verification: Go Zero-Alloc vs Spring Batch Ingestion (10M Records)

Section 1: Go Syntax & Command Blueprint

ใน Go ฟังก์ชัน Benchmark จะถูกเขียนในไฟล์ที่ลงท้ายด้วย _test.go เสมอ และต้องขึ้นต้นชื่อฟังก์ชันด้วย Benchmark... โดยรับพารามิเตอร์เป็น b *testing.B

// internal/ledger/parser_bench_test.go
package ledger_test

import (
    "bytes"
    "testing"
    "ledger-parser/internal/ledger"
)

// Global sink เพื่อป้องกันคอมไพเลอร์ทำ Dead-Code Elimination ตัดลูปทิ้ง!
var benchmarkSink ledger.TransactionRecord

func BenchmarkParseSingleLine(b *testing.B) {
    line := []byte("TX-10001,1711920000,ACC-8801,ACC-9902,500000,THB")
    
    // 1. สั่งเปิดการวัดผล Heap Allocations อัตโนมัติ
    b.ReportAllocs()
    
    // 2. ระบุขนาดไบต์ที่ประมวลผลต่อรอบเพื่อคำนวณ Throughput (MB/s)
    b.SetBytes(int64(len(line)))
    
    // 3. รีเซ็ตตัวจับเวลาเพื่อตัดเวลา Setup ด้านบนออกไป
    b.ResetTimer()

    // 4. ลูปทดสอบ: b.N จะถูก Go Runtime เพิ่มขึ้นเรื่อยๆ จนกระทั่งได้ค่าที่เสถียร
    for i := 0; i < b.N; i++ {
        benchmarkSink = ledger.ParseSingleLine(line)
    }
}

คำสั่งรัน Benchmark และวิเคราะห์ผลลัพธ์ผ่าน Terminal:

$ go test -bench=BenchmarkParseSingleLine -benchmem ./internal/ledger/...
// ผลลัพธ์ที่ได้:
BenchmarkParseSingleLine-16 42857142 28.1 ns/op 1708.20 MB/s 0 B/op 0 allocs/op
  • 42857142: จำนวนรอบที่รันได้ในเวลาประมาณ 1 วินาที (ยิ่งมากยิ่งดี)
  • 28.1 ns/op: เวลาที่ใช้ต่อ 1 Record (28.1 นาโนวินาที = ประมวลผลได้กว่า 35 ล้านแถวต่อวินาที!)
  • 0 B/op, 0 allocs/op: ตัวเลขศักดิ์สิทธิ์ที่ยืนยันว่าไม่มีการแตะต้อง Heap เลยแม้แต่ไบต์เดียว!

Section 2: Memory Architecture & Benchmark Mechanics

ทำไม testing.B ถึงวัดผลได้แม่นยำระดับฮาร์ดแวร์? มาผ่าโครงสร้างของตัวควบคุม Benchmark ภายใน Go Runtime

+---------------------------------------------------------------------------------------+ | testing.B Benchmark Controller State Machine | +---------------------------------------------------------------------------------------+ | 1. b.N = 1 -> รัน 1 ครั้งเพื่อคาดการณ์เวลาต่อรอบ (Estimate run time) | | 2. b.N = 100 -> ขยายรอบเพื่อวอร์มอัปแคช L1/L2 Instruction & Data Cache | | 3. b.N = 10,000 -> ปรับขนาดอัตโนมัติเพื่อให้รอบทดสอบรันอย่างน้อย 1 วินาที (b.benchTime)| | 4. runtime.ReadMemStats -> อ่านค่า TotalAlloc, Mallocs, Frees ทั้งก่อนและหลังลูป | +---------------------------------------------------------------------------------------+ | Calculation: | | ns/op = (TotalElapsedTime) / b.N | | B/op = (TotalAllocBytesAfter - TotalAllocBytesBefore) / b.N | | allocs/op = (TotalMallocCountAfter - TotalMallocCountBefore) / b.N | +---------------------------------------------------------------------------------------+

The Zero-Alloc Audit Aggregator Struct

ในการสรุปรายงานของ Deliverable นี้ เราจะไม่สร้าง Slice หรือ Map เพิ่มเติมระหว่างการสรุปผล แต่จะใช้ Struct Accumulator ตัวเดียวในการกวาดข้อมูล 48 ไบต์ของ TransactionRecord:

type AuditSummary struct {
    TotalRecords    int64 // 8B: จำนวนธุรกรรมทั้งหมด
    ValidRecords    int64 // 8B: จำนวนธุรกรรมที่ผ่านเกณฑ์
    MalformedCount  int64 // 8B: จำนวนแถวที่ฟิลด์ไม่ครบหรือผิดโครงสร้าง
    InvalidAmtCount int64 // 8B: จำนวนรายการที่ยอดเงินไม่ถูกต้อง (<= 0)
    TotalVolumeTHB  int64 // 8B: ยอดเงินรวมในหน่วยสตางค์ (THB)
    TotalVolumeUSD  int64 // 8B: ยอดเงินรวมในหน่วยเซนต์ (USD)
} // ขนาดรวม = 48 ไบต์ (Aligned 8 ไบต์อย่างสมบูรณ์แบบ)

Section 3: Visual Workflow & Assembly/RAM Mechanics

มาดูลำดับขั้นตอนการทำงานจริงระดับ CPU Register และคำสั่ง Assembly ในลูปการคำนวณผลสรุปการเงิน

[CLI Execution Flow: Day 15 Finale] $ ./ledger-cli audit --file=transactions_2026.log | v 1. os.Open(file) -> fd (File Descriptor) | v 2. ledger.ParseJournalStream(file, 100000) |-- bufio.Reader (4KB buffer) |-- Zero-Alloc NextField extraction |-- Direct memory write into []TransactionRecord (Pre-allocated) | v 3. ledger.GenerateAuditSummary(records) |-- Single-pass sequential scan across 48B records in RAM |-- Accumulate counts & integer sums in CPU Registers (ADDQ) | v 4. ledger.PrintTabularReport(os.Stdout, summary) |-- Format ASCII Terminal Table without Reflection / Regex |-- Status: 100% Verified Zero-Alloc Core Processing!

Section 4: Edge Cases, Hazards & Defensive Rules

Hazard #1: The Compiler Dead-Code Trap

หากคุณเขียนลูป Benchmark: for i := 0; i < b.N; i++ { Parse(line) } โดยไม่นำผลลัพธ์ไปกำหนดค่าให้ตัวแปรภายนอก คอมไพเลอร์ที่ฉลาดของ Go จะมองว่าผลลัพธ์ไม่มีใครใช้งาน และจะ ลบโค้ดทั้งฟังก์ชันทิ้งไป (Dead-code elimination) ทำให้ Benchmark รายงานความเร็วเป็น 0.2 ns/op แบบหลอกลวง!
วิธีแก้: กำหนดค่าลงในตัวแปร Global Package Sink เสมอ

Hazard #2: Forgetting b.ResetTimer()

หากมีการโหลดไฟล์จำลองขนาด 50MB ก่อนเริ่มลูป Benchmark แล้วลืมสั่ง b.ResetTimer() เวลาที่ใช้ในการเปิดไฟล์และอ่านดิสก์จะถูกนำไปหารเฉลี่ยรวมใน ns/op ด้วย ทำให้ผลการวัดความเร็วของฟังก์ชัน Parsing คลาดเคลื่อนอย่างรุนแรง

Section 5: Knowledge Verification Quiz

16 Comprehensive Questions — Phase 1 Mastery Evaluation

160 Points Total

Section 6: Hands-on Engineering Labs

ประกอบชิ้นส่วนชุดทดสอบประสิทธิภาพและโมดูลสรุปบัญชีการเงินของ Deliverable ตรวจสอบความถูกต้องผ่าน Verification Engine

Lab 1: Write an Idiomatic Zero-Alloc Benchmark

โจทย์: เขียนฟังก์ชัน BenchmarkIngest(b *testing.B) ที่มีการเรียก b.ReportAllocs(), สั่ง b.ResetTimer() และวนลูป for i := 0; i < b.N; i++ เพื่อเรียก parseRecord(mockData) พร้อมส่งผลลัพธ์เข้าสู่ตัวแปร globalSink เพื่อป้องกัน Dead-Code Elimination

Lab 2: High-Speed In-Memory Ledger Accumulator

โจทย์: เขียนฟังก์ชัน AggregateRecords(records []TransactionRecord) (totalCount int64, validCount int64, totalTHB int64) โดยวนลูปอ่านแบบ Single-pass: หาก rec.Status == 0 ให้นับเป็น validCount และหาก bytes.Equal(rec.Currency[:], []byte("THB")) ให้นำ rec.AmountCents มาบวกสะสมใน totalTHB

Lab 3: Parameterized Sub-benchmarks with b.Run()

โจทย์: เขียนฟังก์ชัน BenchmarkSuite(b *testing.B) โดยใช้ b.Run("ValidLine", ...) และ b.Run("MalformedLine", ...) เพื่อแยกการวัดผลออกเป็น 2 Sub-benchmarks ย่อย

`