ข้ามไปยังเนื้อหา

Profiling & Performance

ก่อนจะ optimise อะไร คุณต้อง วัด ก่อน กฎเดียวกันใช้ได้ทั้งใน TypeScript และ Python: อย่าเดาว่า bottleneck อยู่ที่ไหน — profile ก่อน, optimise ทีหลัง, วัดอีกครั้ง

เครื่องมือ Node.jsคู่เทียบ Python
console.time / console.timeEndtimeit.timeit()
performance.now()time.perf_counter()
node --prof + node --prof-processcProfile + pstats
Chrome DevTools flame graphsnakeviz (pip install snakeviz)
clinic.js / 0xpy-spy (external, sampling profiler)

timeit รัน snippet ซ้ำหลายพันรอบแล้วรายงานเวลารวม บทบาทเทียบได้กับ console.time แต่เข้มงวดกว่ามาก เพราะปิด garbage collection ระหว่างวัดเพื่อตัด noise ออก

TypeScript
// TypeScript — manual timing
console.time("list comp");
for (let i = 0; i < 10_000; i++) {
Array.from({ length: 100 }, (_, i) => i ** 2);
}
console.timeEnd("list comp");
Python
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")
import timeit
# Compare list comprehension vs map vs generator-to-list
approaches = {
"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}")

cProfile คือ deterministic profiler ที่ติดมากับ Python คอยบันทึกทุก function call ว่าแต่ละ function โดนเรียกกี่ครั้งและใช้เวลาไปเท่าไหร่ เทียบได้กับ node --prof ต่างกันตรงที่มีมาให้ในตัวและอ่านผลได้เลยโดยไม่ต้องหา tool เสริม

TypeScript
// Node.js — file-based profiling
// node --prof server.js
// node --prof-process isolate-*.log > profile.txt
Python
import cProfile
import pstats
import io
def slow_function():
total = 0
for i in range(1000):
total += sum(range(i))
return total
# Profile specific function
pr = cProfile.Profile()
pr.enable()
slow_function()
pr.disable()
# Print top 5 by cumulative time
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
ps.print_stats(5)
print(s.getvalue())
import cProfile
import pstats
import 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())
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 ที่ถูกเรียกโดยไม่คาดคิดบ่อยเกินไป
# 1. Profile
import cProfile
cProfile.run('my_function()', 'profile.stats')
# 2. Inspect interactively
import pstats
p = 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

Profiler ตอบคำถามว่า “ทำไมช้า” แต่คำถามอีกแบบคือ “ตอนนี้ process บน production กำลังทำอะไรอยู่” ซึ่งเมื่อก่อนแปลว่าต้อง restart แล้วรันใหม่ใต้ debugger Python 3.14 เพิ่ม interface สำหรับ debugger จากภายนอกแบบปลอดภัย (PEP 768) คุณจึง attach เข้า process ที่รันอยู่ด้วย PID ได้ และไม่มี overhead ตอนที่ไม่ได้ใช้ ฝั่ง Node เทียบได้กับการส่ง SIGUSR1 เพื่อเปิด inspector port

Terminal window
# Attach pdb to a running process (Python 3.14+)
python -m pdb -p 12345
# asyncio apps: see the task tree without stopping anything
python -m asyncio ps 12345 # flat table of tasks
python -m asyncio pstree 12345 # tree of awaiting coroutines

asyncio pstree เป็นวิธีที่เร็วที่สุดในการหา await ที่ค้าง เพราะมันพิมพ์ว่าแต่ละ task รออะไรอยู่ สาย deadlock จึงเห็นได้ตรง ๆ ถ้า deployment ต้องการความเข้มงวด ให้ปิด interface นี้ด้วย PYTHON_DISABLE_REMOTE_DEBUG=1 (หรือส่ง -X disable-remote-debug)

`timeit.timeit(stmt, number=10_000)` return อะไร?
ใน pstats output ของ cProfile `cumtime` หมายความว่าอะไร?
pstats sort key ใดที่หา functions ที่ consume เวลามากที่สุดด้วยตัวเอง (ไม่รวม sub-calls)?
ทำไม timeit จึงปิด garbage collection ระหว่างการรัน?