Profiling & Performance
Mindset ของการ profiling
หัวข้อที่มีชื่อว่า “Mindset ของการ profiling”ก่อนจะ optimise อะไร คุณต้อง วัด ก่อน กฎเดียวกันใช้ได้ทั้งใน TypeScript และ Python: อย่าเดาว่า bottleneck อยู่ที่ไหน — profile ก่อน, optimise ทีหลัง, วัดอีกครั้ง
| เครื่องมือ Node.js | คู่เทียบ Python |
|---|---|
console.time / console.timeEnd | timeit.timeit() |
performance.now() | time.perf_counter() |
node --prof + node --prof-process | cProfile + pstats |
| Chrome DevTools flame graph | snakeviz (pip install snakeviz) |
clinic.js / 0x | py-spy (external, sampling profiler) |
timeit: microbenchmarks ที่แม่นยำ
หัวข้อที่มีชื่อว่า “timeit: microbenchmarks ที่แม่นยำ”timeit รัน snippet ซ้ำหลายพันรอบแล้วรายงานเวลารวม บทบาทเทียบได้กับ console.time แต่เข้มงวดกว่ามาก เพราะปิด garbage collection ระหว่างวัดเพื่อตัด noise ออก
// TypeScript — manual timingconsole.time("list comp");for (let i = 0; i < 10_000; i++) { Array.from({ length: 100 }, (_, i) => i ** 2);}console.timeEnd("list comp");import timeit
# timeit รัน snippet 10,000 ครั้ง; return เวลารวมในหน่วยวินาทีt = timeit.timeit( '[x**2 for x in range(100)]', number=10_000)print(f"List comprehension: {t:.4f}s")
# เปรียบเทียบสองแนวทางt2 = timeit.timeit( 'list(map(lambda x: x**2, range(100)))', number=10_000)print(f"map(): {t2:.4f}s")ลองเล่น — timeit ใน browser
หัวข้อที่มีชื่อว่า “ลองเล่น — timeit ใน browser”import timeit
# Compare list comprehension vs map vs generator-to-listapproaches = { "list comp": '[x**2 for x in range(100)]', "map+list ": 'list(map(lambda x: x**2, range(100)))', "for loop ": 'r=[]; [r.append(x**2) for x in range(100)]',}
results = {}for name, stmt in approaches.items(): t = timeit.timeit(stmt, number=5000) results[name] = t print(f" {name}: {t:.4f}s")
fastest = min(results, key=results.get)print(f"\nFastest: {fastest}")Loading Python runtime (first run only)…
cProfile: function-level profiling
หัวข้อที่มีชื่อว่า “cProfile: function-level profiling”cProfile คือ deterministic profiler ที่ติดมากับ Python คอยบันทึกทุก function call ว่าแต่ละ function โดนเรียกกี่ครั้งและใช้เวลาไปเท่าไหร่ เทียบได้กับ node --prof ต่างกันตรงที่มีมาให้ในตัวและอ่านผลได้เลยโดยไม่ต้องหา tool เสริม
// Node.js — file-based profiling// node --prof server.js// node --prof-process isolate-*.log > profile.txtimport cProfileimport pstatsimport io
def slow_function(): total = 0 for i in range(1000): total += sum(range(i)) return total
# Profile specific functionpr = cProfile.Profile()pr.enable()slow_function()pr.disable()
# Print top 5 by cumulative times = io.StringIO()ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')ps.print_stats(5)print(s.getvalue())cProfile ใน browser
หัวข้อที่มีชื่อว่า “cProfile ใน browser”import cProfileimport pstatsimport io
def inner(n: int) -> int: return sum(range(n))
def middle(n: int) -> int: return sum(inner(i) for i in range(n))
def outer(n: int) -> int: return middle(n) + inner(n)
pr = cProfile.Profile()pr.enable()result = outer(200)pr.disable()
s = io.StringIO()ps = pstats.Stats(pr, stream=s).sort_stats("cumulative")ps.print_stats(8)print(f"Result: {result}")print()print(s.getvalue())Loading Python runtime (first run only)…
การอ่าน pstats output
หัวข้อที่มีชื่อว่า “การอ่าน pstats output”ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.004 0.004 script.py:4(slow_function) 1000 0.004 0.000 0.004 0.000 {built-in method builtins.sum}| Column | ความหมาย |
|---|---|
ncalls | จำนวนครั้งที่ฟังก์ชันถูกเรียก |
tottime | เวลาที่ใช้ใน function นี้ (ไม่รวม callees) |
cumtime | เวลา cumulative (รวมทุก sub-calls) |
percall | เวลาเฉลี่ยต่อ call |
Sort strategies:
sort_stats('cumulative')— หา call chains ที่ช้าที่สุด end-to-end (เริ่มที่นี่)sort_stats('tottime')— หา functions ที่ทำงานมากที่สุดด้วยตัวเองsort_stats('ncalls')— หา functions ที่ถูกเรียกโดยไม่คาดคิดบ่อยเกินไป
Workflow: profile → identify → fix → measure
หัวข้อที่มีชื่อว่า “Workflow: profile → identify → fix → measure”# 1. Profileimport cProfilecProfile.run('my_function()', 'profile.stats')
# 2. Inspect interactivelyimport pstatsp = pstats.Stats('profile.stats')p.sort_stats('cumulative').print_stats(10)
# 3. Visualise (install once)# pip install snakeviz# snakeviz profile.stats # opens browser flame graphเข้าไปดู process ที่ค้างอยู่แล้ว
หัวข้อที่มีชื่อว่า “เข้าไปดู process ที่ค้างอยู่แล้ว”Profiler ตอบคำถามว่า “ทำไมช้า” แต่คำถามอีกแบบคือ “ตอนนี้ process บน production กำลังทำอะไรอยู่” ซึ่งเมื่อก่อนแปลว่าต้อง restart แล้วรันใหม่ใต้ debugger Python 3.14 เพิ่ม interface สำหรับ debugger จากภายนอกแบบปลอดภัย (PEP 768) คุณจึง attach เข้า process ที่รันอยู่ด้วย PID ได้ และไม่มี overhead ตอนที่ไม่ได้ใช้ ฝั่ง Node เทียบได้กับการส่ง SIGUSR1 เพื่อเปิด inspector port
# Attach pdb to a running process (Python 3.14+)python -m pdb -p 12345
# asyncio apps: see the task tree without stopping anythingpython -m asyncio ps 12345 # flat table of taskspython -m asyncio pstree 12345 # tree of awaiting coroutinesasyncio pstree เป็นวิธีที่เร็วที่สุดในการหา await ที่ค้าง เพราะมันพิมพ์ว่าแต่ละ task รออะไรอยู่ สาย deadlock จึงเห็นได้ตรง ๆ ถ้า deployment ต้องการความเข้มงวด ให้ปิด interface นี้ด้วย PYTHON_DISABLE_REMOTE_DEBUG=1 (หรือส่ง -X disable-remote-debug)