Foundationপ্রথম নীতি থেকে
LEVEL 11লেসন ১৬/১৭কঠিন১ ঘণ্টা

Profiling — অনুমান না করে সত্যিকার Bottleneck মাপা

Profiling Tools: perf, Flamegraphs, PMU Counters

গত ১৫টা লেসন ধরে যা তত্ত্বে আলোচনা করেছি — cache miss, branch misprediction, IPC — এই লেসনে সেগুলোকে বাস্তব সংখ্যায় পরিণত করা শেখা: `perf stat`, `perf record`+flamegraph, আর তাদের নিচে বসে থাকা PMU hardware counter দিয়ে।

এই লেসন শেষে আপনি পারবেন

  • `perf stat` চালিয়ে instructions, cycles, IPC, cache-misses, branch-misses-এর মতো hardware counter পড়তে ও সঠিকভাবে ব্যাখ্যা করতে পারবেন
  • `perf record` + `perf report` দিয়ে sampling-based call-graph profiling করে প্রোগ্রামের প্রকৃত hotspot খুঁজে বের করতে পারবেন — অনুমান করা hotspot নয়
  • একটা flamegraph সঠিকভাবে পড়তে পারবেন — width মানে সময়ের অনুপাত, stack depth মানে call depth, left-to-right কোনো সময়ক্রম বহন করে না তা বুঝবেন
  • PMU (Performance Monitoring Unit) আসলে কী — CPU pipeline-এর frontend/backend/retirement স্তরে বসানো hardware counter — তা মাইক্রোআর্কিটেকচার লেসনের সাথে সরাসরি যুক্ত করে ব্যাখ্যা করতে পারবেন
  • Sampling profiler বনাম instrumentation profiler-এর overhead ও accuracy trade-off ব্যাখ্যা করতে পারবেন
  • একটা প্রকৃত `perf stat` output দেখে প্রোগ্রামটা compute-bound, memory-bound, না branch-heavy তা roofline model-এর ভাষায় classify করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

একজন engineer কোড পড়ে বললেন: “বাগ এখানে না, bottleneck ওই sort function-টা — nested loop দেখা যাচ্ছে, ওটাই ধীর হওয়ার কথা।” তিনি তিন দিন ব্যয় করলেন সেই sort-কে আরো দ্রুত করতে। ফলাফল: প্রোগ্রামের মোট সময় ০.৫% ও কমেনি।

তারপর তিনি আসল measurement চালালেন। দেখা গেল প্রোগ্রামের ৭০% সময় যাচ্ছে একটা ছোট্ট, নিরীহ-দেখানো hash lookup function-এ — যেটা এক নজরে দেখলে “অবশ্যই দ্রুত” মনে হয়, কারণ এটা মাত্র তিন লাইন।

এটা কোনো ব্যতিক্রমী গল্প না — এটা প্রতিটা performance engineer-এর প্রথম বড় শিক্ষা। কোড পড়ে অনুমান করা bottleneck আর measure করে পাওয়া bottleneck প্রায়ই সম্পূর্ণ ভিন্ন জিনিস। মানুষের intuition এখানে নির্ভরযোগ্য না — কারণ cache miss, branch misprediction, memory stall — এসবের কোনোটাই কোড পড়ে চোখে দেখা যায় না।

গত ১৫টা লেসনে আমরা যা শিখেছি — microarchitecture-এর frontend/ backend, cache hierarchy, coherence traffic, branch misprediction, আর গত লেসনে roofline model — সবই তত্ত্ব ছিল, hand-calculation ছিল। এই লেসনে আমরা শিখব সেই একই জিনিসগুলো সত্যিকার একটা running program থেকে সরাসরি মাপার উপায় — অনুমান নয়, hardware নিজেই যা গুনেছে।

মূল হাতিয়ার: Linux-এর perf — আর তার নিচে বসে থাকা সিলিকনের একটা অংশ, PMU।

মূল ধারণা

perf — তিনটা আলাদা কাজ, তিনটা আলাদা subcommand

perf একটা একক টুল না, একগুচ্ছ subcommand। এই লেসনে তিনটা মূল subcommand নিয়ে কাজ করব:

Subcommandকাজকখন ব্যবহার
perf statসম্পূর্ণ run-এর aggregate counter summary“প্রোগ্রামটা কি compute-bound না memory-bound?”
perf recordসময়ের সাথে sampling — কোথায় সময় যাচ্ছে“কোন function hotspot?”
perf reportrecord-এর ফলাফল interactive ভাবে দেখাসেই hotspot-এর call chain দেখা

perf stat — উচ্চ-স্তরের সংখ্যা

perf stat ./my_program

একটা বাস্তবসম্মত output (illustrative সংখ্যা):

 Performance counter stats for './my_program':

          1,234.56 msec task-clock                #    0.998 CPUs utilized
                 12      context-switches          #    0.010 K/sec
                  0      cpu-migrations            #    0.000 K/sec
              1,053      page-faults               #    0.853 K/sec
      4,201,332,145      cycles                    #    3.403 GHz
      6,789,001,223      instructions              #    1.62  insn per cycle
      1,245,000,321      branches                  #    1.008 G/sec
         34,567,890      branch-misses             #    2.78% of all branches
        456,789,012      cache-references          #    370.123 M/sec
         89,012,345      cache-misses              #   19.49% of all cache refs

       1.237482318 seconds time elapsed

প্রতিটা লাইন ব্যাখ্যা:

cycles — CPU clock কতবার টিক করলো। instructions — কতগুলো instruction retire (সম্পূর্ণভাবে executed) হলো। এই দুইটার অনুপাত হলো IPC (Instructions Per Cycle) — এই উদাহরণে 6,789,001,223 / 4,201,332,145 ≈ 1.62।

Microarchitecture লেসনে আমরা দেখেছি একটা modern superscalar CPU প্রতি cycle-এ ৪ বা তার বেশি instruction retire করতে পারে (তাত্ত্বিক সর্বোচ্চ IPC ~4-6, CPU-ভেদে)। বাস্তবে IPC 1.62 মানে গড়ে প্রতি cycle-এ কার্যক্ষমতার একটা বড় অংশ অব্যবহৃত — pipeline বেশিরভাগ সময় stall করছে, কোনো না কোনো কারণে instruction আসছে না বা এগোতে পারছে না।

branch-misses — 2.78%। Speculative execution লেসনে আমরা দেখেছি একটা misprediction মানে পুরো pipeline flush — প্রায় ১৫-২০ cycle-এর penalty। 2.78% মিসপ্রেডিকশন rate মাঝারি — খুব খারাপ না (৫-১০% হলে উদ্বেগজনক), কিন্তু IPC কম হওয়ার একটা আংশিক কারণ হতে পারে এটাও।

cache-misses — 19.49% of references। Cache hierarchy লেসনের সংখ্যা মনে করুন — একটা L1 miss মানেই আরো ধীর memory level-এ যাওয়া, শেষ পর্যন্ত DRAM হলে ~২০০ cycle অপেক্ষা। 19.49% একটা উচ্চ miss rate — এটা প্রবলভাবে ইঙ্গিত দেয় প্রোগ্রামটা memory-bound (গত লেসনের roofline ভাষায়), শুধু “ধীর ALU” নয়।

নির্দিষ্ট event বাছাই — -e flag

Default output-এ যা দেখানো হয় তার বাইরেও শত শত event আছে:

perf stat -e cycles,instructions,cache-misses,cache-references,\
branch-misses,branches,LLC-load-misses,dTLB-load-misses \
./my_program

perf list চালালে আপনার নির্দিষ্ট CPU-তে উপলব্ধ সব event দেখা যায় — শত শত entry, কারণ প্রতিটা microarchitecture-এর নিজস্ব counter সেট আছে (মাইক্রোআর্কিটেকচার লেসনের সেই “প্রতিটা প্রজন্মেই নতুন pipeline detail” ধারণাটা এখানেও সত্য)।

perf list-এর output সাধারণত কয়েকটা category-তে ভাগ করা থাকে:

List of pre-defined events (to be used in -e or -M):

  branch-instructions OR branches            [Hardware event]
  cache-misses                               [Hardware event]
  cpu-cycles OR cycles                       [Hardware event]
  instructions                               [Hardware event]
  L1-dcache-load-misses                      [Hardware cache event]
  LLC-load-misses                            [Hardware cache event]
  dTLB-load-misses                           [Hardware cache event]
  context-switches OR cs                     [Software event]
  page-faults OR faults                      [Software event]
  mem_load_retired.l3_miss                   [Kernel PMU event]
  fp_arith_inst_retired.scalar_double        [Kernel PMU event]

“Hardware event” ট্যাগযুক্তগুলো সাধারণ, portable নাম (এই লেসনে যা ব্যবহার করেছি)। “Kernel PMU event” ট্যাগযুক্তগুলো আপনার নির্দিষ্ট CPU model-এর raw event — vendor-নির্দিষ্ট, গভীর বিশ্লেষণে দরকার হয় (পরে hood section-এ বিস্তারিত)। “Software event” ট্যাগযুক্তগুলো PMU থেকে আসে না — সরাসরি kernel scheduler/ memory management থেকে আসা event (যেমন context-switches, page-faults) — এগুলো hardware counter না, তাই counter multiplexing-এর সীমার বাইরে, সবসময় নির্ভুলভাবে গোনা যায়।

perf record + perf report — কোথায় সময় যাচ্ছে

perf stat বলে কী পরিমাণ stall/miss হচ্ছে, কিন্তু কোন function-এ তা বলে না। সেজন্য দরকার sampling:

perf record -g -o perf.data ./my_program
perf report -i perf.data

-g মানে call-graph সহ — শুধু “কোন instruction pointer-এ ছিলাম” তা না, “কোন call stack দিয়ে এখানে পৌঁছালাম” তাও রেকর্ড করা।

Text-based perf report-এর একটা সংক্ষিপ্ত output:

# Overhead  Command     Shared Object      Symbol
# ........  ..........  .................  ........................
#
    45.23%  my_program  my_program         [.] hash_lookup
    22.10%  my_program  my_program         [.] quicksort_partition
    12.87%  my_program  libc.so.6          [.] memcpy
     8.45%  my_program  my_program         [.] main
     6.12%  my_program  my_program         [.] rebalance_tree
     3.90%  my_program  [kernel.kallsyms]  [k] page_fault
     1.33%  my_program  my_program         [.] logger_flush

এইটাই সেই humbling মুহূর্ত — quicksort_partition, যেটা “দেখতেই জটিল” ছিল, মাত্র 22.10%। আর hash_lookup, যেটা তিন লাইনের কোড, 45.23% — অর্ধেকেরও বেশি সময়।

কেন? সম্ভাব্য কারণ — hash function নিজে সস্তা, কিন্তু প্রতিটা call একটা cache miss ঘটাচ্ছে কারণ hash table বড়, randomly accessed, আর cache-এ আঁটছে না। কোড ছোট হলেও, প্রতিটা ছোট্ট call-এর আড়ালে একটা ~২০০ cycle DRAM wait লুকিয়ে আছে — যেটা কোড পড়ে দেখা যায় না, শুধু measure করেই ধরা যায়।

perf top — লাইভ, রিয়েল-টাইম hotspot দেখা

perf record + perf report একটা post-mortem workflow — আগে রেকর্ড, পরে বিশ্লেষণ। মাঝে মাঝে দরকার একটা লাইভ view — এখনই সিস্টেমে কী চলছে, কোন function সবচেয়ে বেশি CPU খাচ্ছে — top কমান্ডের মতো, কিন্তু process-level না, function-level:

sudo perf top

একটা continuously-updating টার্মিনাল UI দেখায়:

  PerfTop:   4823 irqs/sec  kernel: 12.3%  exact:  99.8% [4000Hz cycles]
-------------------------------------------------------------------

    45.91%  my_program        [.] hash_lookup
    18.22%  libc.so.6         [.] memcpy
    11.05%  my_program        [.] rebalance_tree
     8.14%  [kernel.kallsyms] [k] finish_task_switch
     5.30%  my_program        [.] main

এটা বিশেষভাবে কাজে লাগে যখন একটা এখনই চলমান production সমস্যা তদন্ত করছেন — কোনো perf.data ফাইল সংরক্ষণ না করেই মুহূর্তেই বোঝা যায় কোন function সবচেয়ে গরম, যা perf record + perf report সাইকেলের অপেক্ষা এড়ায়। ট্রেড-অফ: perf top-এর সংখ্যা মুহূর্তের একটা rolling average — একটা নির্দিষ্ট সময়ের পুরো history বিশ্লেষণের জন্য perf record এখনো প্রয়োজন।

ভেতরে কী ঘটছে

PMU — এই সব সংখ্যা কোথা থেকে আসছে

PMU (Performance Monitoring Unit) কোনো software বিমূর্ততা না — এটা প্রতিটা CPU core-এর ভেতরে বসানো আসল সিলিকন, মাইক্রোআর্কিটেকচার লেসনের সেই frontend → backend → retirement pipeline-এর প্রতিটা স্তরে counter বসানো:

PMU counter কোথায় বসানো — pipeline stage অনুযায়ী
  1. Frontend countersfetch stall, decode stall, branch misprediction — instruction pipeline-এ ঢোকার আগেই
  2. Backend countersexecution port utilization, load/store buffer full, ALU busy cycle
  3. Memory subsystem countersL1/L2/L3 hit-miss, TLB hit-miss, memory controller queue depth
  4. Retirement countersinstructions retired, uops retired, cycles — pipeline-এর একদম শেষ ধাপ

প্রতিটা counter একটা programmable hardware register — model-specific register (MSR)। OS (perf_events subsystem, Linux kernel-এর একটা অংশ) এই MSR-গুলো প্রোগ্রাম করে বলে দেয় কোন event গুনতে হবে, তারপর সেই register-এর মান পড়ে।

দুইটা mode:

  • Counting mode (perf stat) — counter শুরু থেকে শেষ পর্যন্ত বাড়তেই থাকে, run শেষে একবার পড়া হয়। কোনো overhead নেই বললেই চলে — hardware নিজে গুনছে, software intervene করছে না।
  • Sampling mode (perf record) — counter একটা নির্দিষ্ট মান থেকে countdown করে (যেমন প্রতি 10,000,000 cycle), শূন্যে পৌঁছালে একটা hardware interrupt তোলে। Kernel সেই interrupt ধরে, ওই মুহূর্তের instruction pointer আর call stack রেকর্ড করে, counter আবার রিসেট করে দেয়। হাজার হাজার sample জমিয়ে একটা পরিসংখ্যানগত ছবি তৈরি হয় — কোথায় বেশি sample পড়েছে, সেখানেই বেশি সময় কাটানো হয়েছে বলে অনুমান।

Stack unwinding — কেন কখনো কখনো call graph ভাঙা দেখায়

Sample নেওয়ার মুহূর্তে শুধু current instruction pointer জানা যথেষ্ট না — কোন function-টা কাকে call করে এখানে এসেছে তা জানতে পুরো call stack “unwind” করতে হয়। তিনটা পদ্ধতি:

  1. Frame pointer chasing — প্রতিটা stack frame-এ একটা saved base pointer (rbp) থাকলে সেটা অনুসরণ করে দ্রুত unwind করা যায়। সমস্যা: -O2-এ compiler ডিফল্টভাবে rbp-কে সাধারণ register হিসেবে পুনর্ব্যবহার করে (frame pointer omission), তখন unwind ভেঙে যায় — perf report-এ শুধু একটামাত্র flat frame দেখা যায়, পুরো call chain হারিয়ে যায়।
  2. DWARF-based unwinding — debug info থেকে সঠিকভাবে stack বোঝা যায় frame pointer ছাড়াই, কিন্তু ধীর ও বেশি data লাগে (perf record --call-graph dwarf)।
  3. LBR (Last Branch Record) — একটা বিশেষ Intel hardware feature যা সাম্প্রতিক branch-গুলোর একটা hardware buffer-এ রাখে, ফলে বাড়তি overhead ছাড়াই সঠিক call chain পাওয়া যায় (perf record --call-graph lbr)।

Fix — compile করার সময় frame pointer রাখুন:

gcc -O2 -fno-omit-frame-pointer -g -o my_program my_program.c

এই একটা flag ভুলে গেলে পুরো profiling session অর্থহীন data দেয় — বাস্তবে এটাই সবচেয়ে সাধারণ প্রথম-বার profiling ব্যর্থতার কারণ।

/proc-এর সাথে সম্পর্ক

perf symbol resolution (address থেকে function name বের করা) করতে প্রক্রিয়ার memory map জানতে হয় — সেটা আসে /proc/<pid>/maps থেকে (operating-systems module-এর “/proc আর /sys” লেসনে আমরা এই ফাইলটাই সরাসরি হাতে পড়েছিলাম)। কোন shared library কোথায় load হয়েছে তা এখান থেকেই জানা যায়, তাই একটা sample-এর instruction pointer-কে সঠিক function name-এ map করা সম্ভব হয়।

আরেকটা ব্যবহারিক ফাইল — /proc/sys/kernel/perf_event_paranoid। Unprivileged user হিসেবে perf চালাতে গেলে প্রায়ই একটা permission error আসে; এই ফাইলের মান (-1 থেকে 4) নির্ধারণ করে সাধারণ user কতটা গভীর hardware counter access পাবে। অনেক container/CI পরিবেশে এটা কড়াভাবে সেট করা থাকে, যা perf ব্যবহারকারীদের একটা সাধারণ প্রথম বাধা।

Counter multiplexing — যখন আপনি counter-এর চেয়ে বেশি event চান

প্রতিটা core-এ সাধারণত মাত্র ৪-৮টা general-purpose hardware counter register থাকে (কিছু fixed-function counter আলাদা)। আপনি যদি perf stat -e-তে ১৫টা event চান, hardware সবগুলো একসাথে গুনতে পারে না — kernel সেগুলোকে সময় ভাগ করে (multiplex) — পালাক্রমে কয়েকটা event একবারে গুনে, বাকিটা estimate করে scale-up করে। perf stat output-এ তখন একটা “(XX.XX%)” annotation দেখা যায় প্রতিটা event-এর পাশে — সেটাই বলে দেয় কত শতাংশ সময় সেই event আসলে গোনা হয়েছিল, বাকিটা extrapolation।

Vendor-ভেদে event নাম আলাদা — কেন perf list প্রথমে দেখা জরুরি

মাইক্রোআর্কিটেকচার লেসনে আমরা দেখেছি প্রতিটা CPU vendor (এমনকি একই vendor-এর প্রতিটা generation) pipeline-এর বিস্তারিত ভিন্ন রাখে — PMU event নামেও এই পার্থক্য প্রতিফলিত হয়:

ধারণাIntel raw eventAMD raw eventGeneric alias (perf-এর নিজের)
L1 data cache missmem_load_retired.l1_missl1_dtlb_misses (ভিন্ন semantics)L1-dcache-load-misses
Retired FP instructionfp_arith_inst_retiredfp_ret_sse_avx_ops(কোনো universal alias নেই)
Branch mispredictionbr_misp_retired.all_branchesbp_de_redirectbranch-misses

perf-এর generic alias (ডান কলামের পাশের branch-misses, cache-misses-এর মতো নাম, যা এই পুরো লেসনে আমরা ব্যবহার করেছি) vendor-নিরপেক্ষ, kernel দ্বারা সবচেয়ে কাছাকাছি raw event-এ map করা — সাধারণ ব্যবহারের জন্য যথেষ্ট। কিন্তু গভীর, নির্দিষ্ট microarchitectural প্রশ্নে (যেমন “L2-তে ঠিক কোন ধরনের miss — demand load না prefetch?”) raw event নাম দরকার হয়, যা শুধু perf list চালিয়ে আপনার নির্দিষ্ট CPU-তে যাচাই করা যায় — একটা laptop-এ যা কাজ করে, একটা ভিন্ন generation-এর server-এ হুবহু একই command কাজ নাও করতে পারে।

perf annotate — instruction-level attribution

perf report function-level granularity দেয়। এক ধাপ গভীরে যেতে — একটা function-এর কোন নির্দিষ্ট instruction-এ সময় যাচ্ছে — perf annotate ব্যবহার হয়:

perf annotate hash_lookup
       │  hash_lookup():
  2.1% │  mov    %edi,%eax
  1.8% │  imul   $0x9e3779b1,%eax,%eax
 78.4% │  mov    (%rbx,%rax,8),%rdx      ← এখানেই ৭৮% সময়
 12.3% │  mov    %rdx,%rax
  5.4% │  ret

এই output স্পষ্টভাবে দেখাচ্ছে — hash গণনা (multiply) নিজে সস্তা (1.8%), কিন্তু ঠিক তার পরের memory load instruction-টাই ((%rbx, %rax,8) — table-এ random index access) 78.4% সময় খাচ্ছে। এটা এই লেসনের earlier উদাহরণের ঠিক hardware-level প্রমাণ — cache miss কোথায় লুকিয়ে আছে তা instruction-level এ দেখা যাচ্ছে, শুধু function-level এ না।

Off-CPU time — perf-এর একটা অন্ধ বিন্দু

এই পুরো লেসনে যা বর্ণনা করা হয়েছে (perf record-এর PMU-driven sampling) একটা গুরুত্বপূর্ণ শর্তে কাজ করে — thread-টা core-এ সত্যিই চলছে থাকতে হবে। PMU counter শুধু তখনই overflow interrupt তুলতে পারে যখন সেই core-এ instruction retire হচ্ছে।

কিন্তু একটা thread প্রায়ই block হয়ে থাকে — একটা mutex-এর জন্য অপেক্ষা, একটা disk read সম্পূর্ণ হওয়ার অপেক্ষা, বা একটা network response-এর অপেক্ষা। এই সময়টায় thread-টা scheduler দ্বারা core থেকে সরিয়ে দেওয়া হয়েছে (blocked/sleeping state) — কোনো instruction retire হচ্ছে না, তাই কোনো PMU sample-ও পড়ছে না।

ফলাফল: যদি একটা function-এর বেশিরভাগ wall-clock সময় আসলে lock contention-এ অপেক্ষায় কাটে (race-conditions-and-deadlock লেসনের বিষয়বস্তু), perf record-এর ফলাফলে সেই function প্রায় অদৃশ্য দেখাবে — কারণ CPU-তে সে সত্যিই কিছু করছিল না, শুধু অপেক্ষা করছিল। একটা naive উপসংহার হতে পারে “এই function সমস্যা না, ওর CPU সময় তো কম” — যেখানে আসল সমস্যা ঠিক ওখানেই, শুধু ভিন্ন রূপে (contention, CPU সময় না)।

সমাধান — off-CPU profiling। Brendan Gregg-এর জনপ্রিয় করা এই কৌশল PMU counter-এর বদলে kernel scheduler-এর context-switch event ব্যবহার করে — প্রতিবার একটা thread block/unblock হয়, সেই মুহূর্তের timestamp আর stack trace রেকর্ড করা হয়:

perf sched record -- ./my_program
perf sched latency

অথবা eBPF-ভিত্তিক আধুনিক টুল (offcputime from bcc/BPF-tools) সরাসরি “কোন stack, কতক্ষণ ব্লকড ছিল” এর একটা flamegraph বানায় — ঠিক perf record-এর মতোই দেখতে, কিন্তু width এখন wall-clock wait time বোঝায়, CPU time না।

উদাহরণ

পূর্ণাঙ্গ বিশ্লেষণ — pointer chasing পুনরায়

Level 0-এর asymptotic notation লেসনে আমরা sequential বনাম random pointer-chasing traversal তুলনা করেছিলাম আর দেখেছিলাম random ১০-২০ গুণ ধীর। তখন perf stat command-টা শুধু উল্লেখ করা হয়েছিল — এখন পুরো output ব্যাখ্যা করি।

perf stat -e cache-misses,cache-references,cycles,instructions ./chase

Sequential access-এ (illustrative):

        12,345,678      cache-misses              #    2.1% of all cache refs
       587,654,321      cache-references
     1,203,456,789      cycles
     1,890,123,456      instructions              #    1.57  insn per cycle

Random access-এ, একই মোট step সংখ্যায়:

       445,123,987      cache-misses              #   75.8% of all cache refs
       587,001,234      cache-references
     8,912,345,678      cycles
     1,890,456,789      instructions              #    0.21  insn per cycle

তুলনা করি: instruction সংখ্যা প্রায় অভিন্ন (~1.89 বিলিয়ন দুই ক্ষেত্রেই — একই কাজ, একই loop count)। কিন্তু cache-miss rate 2.1% থেকে 75.8%-এ লাফ দিয়েছে, আর সেই কারণে cycles প্রায় ৭.৪ গুণ বেড়ে গেছে, IPC 1.57 থেকে 0.21-এ ধসে পড়েছে।

এটাই সেই সংখ্যা যা Level 0-তে শুধু “প্রায় ১০-২০ গুণ ধীর” বলে বর্ণনা করা হয়েছিল — এখন hardware counter দিয়ে ঠিক কেন তা দেখানো গেল। একই instruction সংখ্যা, সম্পূর্ণ ভিন্ন cache আচরণ, সম্পূর্ণ ভিন্ন wall-clock সময়। Asymptotic complexity (Θ(n) দুই ক্ষেত্রেই) এই পার্থক্যের কিছুই ধরতে পারে না — শুধু perf পারে।

SequentialRandomঅনুপাত
Instructions1.89 B1.89 B1.00×
Cache-miss rate2.1%75.8%—
Cycles1.20 B8.91 B7.42×
IPC1.570.210.13×
একই instruction সংখ্যা, ভিন্ন IPC — কারণ perf counter-এই দৃশ্যমান।

Flamegraph — কীভাবে পড়তে হয়

perf record-এর raw output text-based perf report-এ পড়া যায়, কিন্তু গভীর call tree-তে সেটা কষ্টকর। Brendan Gregg-এর flamegraph সেই একই data-কে একটা ছবিতে রূপান্তর করে:

perf record -g -o perf.data ./my_program
perf script -i perf.data > out.perf
stackcollapse-perf.pl out.perf > out.folded
flamegraph.pl out.folded > out.svg
┌───────────────────────────────────────────────────────────┐
│                      hash_lookup (45%)                     │  ← leaf, top
├──────────────────────┬──────────────────────────────────────┤
│  rebalance_tree (10%)│        hash_lookup callers (35%)      │
├───────┬──────────────┴────────────┬─────────────────────────┤
│ insert│      process_batch (45%)                            │
│ (8%)  │                                                      │
├───────┴──────────────────────────────────────────────────────┤
│                         main (100%)                          │  ← base
└───────────────────────────────────────────────────────────────┘
     ↑ প্রতিটা বাক্সের প্রস্থ = মোট sample-এর কত শতাংশ এই ফাংশনে ছিল
একটা flamegraph-এর গঠন — ASCII অনুকরণ।

পড়ার নিয়ম, তিনটা মূল বিষয়:

১. Width = সময়, বাম-ডান ক্রম কিছু বোঝায় না। একটা function-এর বাক্স যত চওড়া, তার (নিজের + তার সব child-এর) sample তত বেশি — অর্থাৎ মোট সময়ের ভাগ তত বেশি। বাক্সগুলো সাধারণত alphabetically সাজানো হয়, বাম থেকে ডানে সময়ের ক্রম অনুযায়ী না — তাই “বাঁ দিকে যা আছে সেটা আগে ঘটেছে” ভাবাটা ভুল।

২. Stack depth = call depth। নিচে main (root/base), উপরে যত যাবেন তত গভীর call chain — সবচেয়ে উপরের বাক্সগুলোই ঐ মুহূর্তে CPU-তে সরাসরি executing (leaf function), সব sample-এর।

৩. একটা বাক্সের প্রস্থ তার নিজের কাজ + তার সব সন্তানের কাজ অন্তর্ভুক্ত করে (inclusive/total সময়), যতক্ষণ না নিচের দিকে নতুন সরু ভাগ দেখানো হচ্ছে child function-গুলোর জন্য। যদি একটা বাক্সের উপরে কোনো child বাক্স না থাকে (একদম উপরের স্তর), তার পুরো প্রস্থটাই সেই function-এর “self time” — নিজে সরাসরি খরচ করা সময়।

রঙ সাধারণত অর্থহীন — বেশিরভাগ flamegraph tool এলোমেলো (random, কিন্তু consistent) রঙ দেয় শুধু পাশাপাশি বাক্স আলাদা করে চেনার জন্য। ব্যতিক্রম: differential flamegraph, যেখানে লাল/নীল রঙ দুইটা profile-এর মধ্যে পার্থক্য (regression/improvement) নির্দেশ করে।

নিজে চালিয়ে দেখুন

EXPERIMENT

অনুমান বনাম measurement — নিজের হাতে humbling মুহূর্তটা দেখুন

Linux, gcc, linux-tools-common (perf)· ২৫ মিনিট

একটা প্রোগ্রাম যেখানে “দেখতে জটিল” আর “আসলে ধীর” আলাদা:

#include \<stdio.h>
#include \<stdlib.h>
#include \<string.h>

#define TABLE_SIZE 1000003   // মৌলিক সংখ্যা, বড়, cache-এ আঁটে না
#define N 5000000

typedef struct { int key; int val; } Entry;
Entry table[TABLE_SIZE];

// দেখতে "সরল" — কিন্তু random access pattern
int hash_lookup(int key) {
    int idx = (key * 2654435761u) % TABLE_SIZE;   // Knuth multiplicative hash
    return table[idx].val;                          // random location — cache miss-প্রবণ
}

// দেখতে "জটিল" — nested loop, কিন্তু ছোট, in-cache array
void quicksort_partition(int *arr, int lo, int hi) {
    if (lo >= hi) return;
    int pivot = arr[hi], i = lo - 1;
    for (int j = lo; j \< hi; j++) {
        if (arr[j] \< pivot) { i++; int t = arr[i]; arr[i] = arr[j]; arr[j] = t; }
    }
    int t = arr[i+1]; arr[i+1] = arr[hi]; arr[hi] = t;
    quicksort_partition(arr, lo, i);
    quicksort_partition(arr, i + 2, hi);
}

int main(void) {
    for (int i = 0; i \< TABLE_SIZE; i++) { table[i].key = i; table[i].val = i * 3; }

    long long sum = 0;
    for (long i = 0; i \< N; i++) sum += hash_lookup((int)(i * 7919));

    int small[64];
    for (int r = 0; r \< 200000; r++) {
        for (int i = 0; i \< 64; i++) small[i] = (i * 37 + r) % 997;
        quicksort_partition(small, 0, 63);
    }

    printf("sum=%lld small[0]=%d\n", sum, small[0]);
    return 0;
}
gcc -O2 -fno-omit-frame-pointer -g -o hotspot_demo hotspot_demo.c
perf record -g -o perf.data ./hotspot_demo
perf report -i perf.data --stdio | head -20

সাধারণ (illustrative) output:

# Overhead  Symbol
# ........  ........................
    82.14%  [.] hash_lookup
    11.35%  [.] quicksort_partition
     4.02%  [.] main
     2.49%  [.] __libc_start_main

hash_lookup — ৩ লাইনের function, 82% সময়। quicksort_partition — nested loop সহ recursive sort, মাত্র 11%।

perf stat-এ কারণটা নিশ্চিত করুন:

perf stat -e cache-misses,cache-references,instructions,cycles ./hotspot_demo

hash_lookup-এর প্রতিটা call TABLE_SIZE-ব্যাপী ছড়ানো একটা বড় array-তে random index access করে — array cache-এ আঁটে না, তাই প্রায় প্রতিটা call একটা DRAM trip। quicksort_partition-এর array মাত্র ৬৪ element (256 byte) — সম্পূর্ণ L1-এ আঁটে, তাই যতই nested loop থাকুক, প্রতিটা access প্রায় বিনামূল্যে।

সিদ্ধান্ত না, measurement-ই সত্য বলে।

এটা কী প্রমাণ করে

কোড পড়ে 'সবচেয়ে জটিল দেখানো' function-টাই যে আসল bottleneck হবে এমন কোনো নিশ্চয়তা নেই — প্রকৃত hotspot খুঁজতে measurement বাধ্যতামূলক, intuition যথেষ্ট না।

নিজে বানান

BUILD IT

মিনি Sampling Profiler — perf record নিজে বানিয়ে দেখুন ছোট আকারে

C (POSIX signals) · ●●●●○
  1. setitimer(ITIMER_PROF, ...) দিয়ে একটা পর্যায়ক্রমিক CPU-time interval timer সেট করুন
  2. SIGPROF signal handler-এ প্রতিবার বর্তমান instruction pointer রেকর্ড করুন
  3. হাজার হাজার sample জমার পর, কোন address-এ কত sample পড়েছে তার একটা histogram বানান
  4. address-কে function name-এ map করুন (symbol table বা /proc/self/maps থেকে), আর percentage হিসেবে প্রিন্ট করুন

perf record কার্যত কী করে তার একটা সরলীকৃত, userspace সংস্করণ — hardware PMU interrupt-এর বদলে POSIX timer signal ব্যবহার করে।

#include \<stdio.h>
#include \<stdlib.h>
#include \<string.h>
#include \<signal.h>
#include \<sys/time.h>
#include \<ucontext.h>

#define MAX_SAMPLES 100000
static void *samples[MAX_SAMPLES];
static volatile int n_samples = 0;

void sigprof_handler(int sig, siginfo_t *info, void *ucontext) {
    ucontext_t *ctx = (ucontext_t *)ucontext;
#if defined(__x86_64__)
    void *pc = (void *)ctx->uc_mcontext.gregs[REG_RIP];   // বর্তমান instruction pointer
#else
    void *pc = NULL;
#endif
    if (n_samples \< MAX_SAMPLES) samples[n_samples++] = pc;
}

void start_profiler(int hz) {
    struct sigaction sa = {0};
    sa.sa_sigaction = sigprof_handler;
    sa.sa_flags = SA_SIGINFO | SA_RESTART;
    sigaction(SIGPROF, &sa, NULL);

    struct itimerval timer;
    long usec = 1000000 / hz;                 // যেমন hz=1000 → প্রতি ১ms একটা sample
    timer.it_value.tv_sec = 0;
    timer.it_value.tv_usec = usec;
    timer.it_interval.tv_sec = 0;
    timer.it_interval.tv_usec = usec;
    setitimer(ITIMER_PROF, &timer, NULL);      // CPU time-ভিত্তিক — wall-clock না
}

void report(void) {
    printf("\nমোট sample: %d\n", n_samples);
    printf("(প্রকৃত tool-এ এখানে /proc/self/maps পড়ে address → function\n");
    printf(" name resolve করা হয়, আর duplicate address-এর count নিয়ে\n");
    printf(" percentage বের করা হয় — এটাই perf report-এর মূল যুক্তি)\n");
}

// ── ব্যবহারের উদাহরণ ──
long long busy_work(long n) {
    long long s = 0;
    for (long i = 0; i \< n; i++) s += i * i;
    return s;
}

int main(void) {
    start_profiler(1000);           // প্রতি সেকেন্ডে ~১০০০ sample
    long long total = 0;
    for (int i = 0; i \< 50; i++) total += busy_work(2000000);
    report();
    printf("total=%lld\n", total);
    return 0;
}
gcc -O2 -g -o mini_profiler mini_profiler.c
./mini_profiler

মূল শিক্ষা: আসল perf record এই একই ধারণা ব্যবহার করে, তবে তিনটা বড় পার্থক্যসহ — (১) SIGPROF timer-এর বদলে hardware PMU counter overflow interrupt ব্যবহার করে, যা wall-clock না, প্রকৃত CPU cycle/instruction-এর উপর ভিত্তি করে trigger হয় ও অনেক কম overhead-এর; (২) kernel-space-এ চলে, তাই সব process/thread একসাথে দেখতে পারে, শুধু নিজের process না; (৩) frame pointer/DWARF/LBR দিয়ে পুরো call stack unwind করে, শুধু একটা instruction pointer না।

নিজে বাড়ান:

  1. /proc/self/maps পড়ে প্রতিটা sample address-কে library/binary অংশে map করুন
  2. dladdr() ব্যবহার করে address থেকে function name বের করার চেষ্টা করুন (dynamically linked function-এ কাজ করবে)
  3. একটা দ্বিতীয় “hot” function যোগ করে দেখুন sample distribution কীভাবে বদলায়
  4. Sample rate (hz) বাড়িয়ে-কমিয়ে দেখুন overhead ও resolution-এর trade-off

বাস্তব সিস্টেমে

Profiling যেখানে দৈনন্দিন কাজে লাগে

Continuous profiling — Google, Meta, Netflix। বড় কোম্পানিগুলো শুধু “যখন সমস্যা হয় তখন profile করি” মডেল ছেড়ে পুরো production fleet-এ সবসময় কম-overhead sampling চালায় (Google-এর অভ্যন্তরীণ সিস্টেম, বা open-source Pyroscope/Parca-এর মতো টুল) — যাতে একটা performance regression deploy হওয়ার সাথে সাথেই flamegraph diff-এ ধরা পড়ে, কারো manual profiling শুরু করার অপেক্ষা ছাড়াই।

Netflix-এর performance war room। Brendan Gregg (flamegraph-এর উদ্ভাবক) Netflix-এ থাকাকালীন বিখ্যাতভাবে production latency spike তদন্ত করতেন সরাসরি live server-এ perf record চালিয়ে, মিনিটের মধ্যে flamegraph দেখে exact culprit ধরে ফেলতেন — এই পদ্ধতিটাই আজ পুরো industry-র standard practice।

JVM/Java profiling — async-profiler। Java-র মতো managed runtime-এ traditional sampling কঠিন (JIT-compiled code, garbage collector pause) — async-profiler ঠিক এই লেসনের PMU + perf_events mechanism ব্যবহার করেই কাজ করে, শুধু JVM-এর symbol table বুঝতে অতিরিক্ত কাজ করে।

Database query optimization। PostgreSQL-এর একটা ধীর query EXPLAIN ANALYZE-এ ঠিকঠাক দেখালেও, আসল সময় কোথায় যাচ্ছে (lock wait, buffer I/O, নাকি CPU-bound sort) তা নিশ্চিত করতে DBA-রা সরাসরি perf record -p <postgres_pid> চালান — অনুমান না করে।

eBPF-ভিত্তিক আধুনিক টুল। bpftrace, bcc-এর মতো টুল একই PMU/perf_events infrastructure ব্যবহার করে, কিন্তু kernel-এর ভেতরে নিজস্ব ছোট প্রোগ্রাম চালিয়ে আরো নমনীয় (custom filtering, in-kernel aggregation), কম overhead প্রোফাইলিং করে — perf-এর যুক্তিরই বিবর্তিত রূপ।

Compiler ও language runtime দল। GCC/LLVM/V8 দলগুলো নিজেদের optimization pass-এর প্রভাব যাচাই করতে প্রতিটা commit-এ flamegraph diff দেখে — একটা “optimization” আসলে regression কি না তা বিশ্বাসের বদলে measurement দিয়ে confirm করা হয়।

Cloud cost optimization। একটা service-এর CPU utilization বেশি মানেই বেশি instance দরকার — কিন্তু profiling প্রায়ই দেখায় সেই CPU সময়ের বড় অংশ একটা অপ্রয়োজনীয়, optimize-যোগ্য hotspot-এ যাচ্ছে (যেমন redundant serialization/deserialization), যা ঠিক করলে instance সংখ্যা না বাড়িয়েই traffic বৃদ্ধি সামলানো যায়।

Kernel developer-দের দৈনন্দিন কাজ। Linux kernel-এর নিজস্ব performance-critical path (scheduler, network stack, filesystem) kernel developer-রা নিজেরাই perf-এর প্রথম ব্যবহারকারী ও maintainer — kernel-এর প্রতিটা major release-এর আগে perf record দিয়ে regression খোঁজা একটা standard ধাপ। এটা যৌক্তিক — যে টুল নিজেই kernel-এর অংশ, সেটা kernel-এর নিজের কোড profile করার জন্যই সবচেয়ে ভালো উপযোগী।

Incident response/SRE workflow। একটা production outage চলাকালীন, যখন প্রতিটা মিনিট গুরুত্বপূর্ণ, SRE team-এর প্রথম কয়েকটা কমান্ডের মধ্যে প্রায়ই থাকে perf top বা একটা সংক্ষিপ্ত perf record — কোনো deep investigation-এর আগেই একটা দ্রুত সংকেত পেতে “CPU কোথায় যাচ্ছে”, যা প্রায়ই তদন্তকে সঠিক দিকে প্রথম ধাক্কা দেয়।

যে ভুলগুলো সবাই করে

“যে function কোডে সবচেয়ে জটিল/nested দেখায়, সেটাই bottleneck।”

এই লেসনের ভূমিকার গল্পই এর প্রমাণ, আর Experiment section-এ আমরা নিজেরাই এটা পুনরুৎপাদন করেছি — quicksort_partition (nested loop, recursive) মাত্র 11% সময় নিয়েছিল, আর তিন-লাইনের hash_lookup নিয়েছিল 82%।

কারণ: কোড জটিলতা CPU cycle-এর সাথে সরাসরি সম্পর্কিত না। যা সম্পর্কিত তা হলো — কত cache miss, কত branch misprediction, কতবার call হচ্ছে, memory access pattern কেমন। এসবের কোনোটাই সোর্স কোড পড়ে চোখে দেখা যায় না।

নিয়ম: optimization শুরু করার আগে সবসময় প্রথমে profile করুন। Knuth-এর সেই “critical 3% খুঁজে বের করাই আসল কাজ” বাক্যের ব্যবহারিক রূপায়ণ এটাই — perf দিয়ে সেই ৩% খুঁজে বের করা, অনুমান না করা।

“Profiler ব্যবহার করলে প্রোগ্রাম এত ধীর হয়ে যায় যে ফলাফল অবিশ্বাস্য হয়ে পড়ে।”

এটা সত্য instrumentation profiler-এর জন্য (যেমন পুরনো gprof, যা প্রতিটা function call-এ একটা counter-increment কোড insert করে compile-time-এ) — এর overhead ২-১০× পর্যন্ত হতে পারে, আর সেটা নিজেই measurement-কে বিকৃত করে ফেলে (এটাকে বলে “probe effect” — পর্যবেক্ষণ নিজেই যা পর্যবেক্ষণ করছে তা বদলে দেয়)।

perf-এর মতো sampling profiler ভিন্ন। এটা প্রোগ্রামের কোডে কিছু insert করে না — hardware PMU নিজে থেকেই গুনছে, kernel শুধু পর্যায়ক্রমে (যেমন প্রতি সেকেন্ডে ১০০০ বার) একটা snapshot নেয়। Typical overhead ১-৫%-এর মধ্যে — production সার্ভারেও নিরাপদে চালানো যায়, যেমন Netflix-এর মতো কোম্পানি লাইভ ট্রাফিকেই চালায়।

Trade-off: sampling কম overhead দেয়, কিন্তু পরিসংখ্যানগত (আগের Callout-এ উল্লেখিত সীমা), instrumentation নিখুঁত count দেয় কিন্তু নিজেই বিকৃতিকারী। সঠিক পছন্দ পরিস্থিতির উপর নির্ভরশীল — কিন্তু production diagnosis-এ প্রায় সবসময় sampling জেতে।

“perf যা রিপোর্ট করে তা ১০০% নির্ভুল, কোনো সন্দেহের অবকাশ নেই।”

তিনটা বাস্তব সীমা মনে রাখা জরুরি।

১. Sampling noise — খুব অল্প sample সংখ্যায় (ছোট run, বা খুব কম sample rate-এ) percentage-গুলোয় যথেষ্ট পরিসংখ্যানগত ভুল থাকতে পারে। বেশি sample = বেশি নির্ভরযোগ্যতা।

২. Inlining-এর প্রভাব — যদি compiler একটা ছোট function-কে তার caller-এ inline করে দেয় (-O2/-O3-এ খুবই সাধারণ), সেই function আলাদা symbol হিসেবে profile-এ দেখাই যাবে না — তার সময় caller-এর নামে যোগ হয়ে যাবে। এটা কোনো bug না, বরং একটা reminder যে profile আসলে compiled binary-র structure দেখায়, source code-এর না।

৩. Counter multiplexing — অনেক event একসাথে চাইলে hardware সবগুলো একটানা গুনতে পারে না (আগেই আলোচিত), তখন কিছু সংখ্যা extrapolated estimate, exact count না।

৪. Off-CPU time সম্পূর্ণ অদৃশ্য — hood section-এ আলোচিত, perf record-এর সাধারণ (on-CPU) mode একটা thread block/অপেক্ষায় থাকা অবস্থায় কোনো sample-ই নেয় না। একটা lock-contention-ভারী function তাই profile-এ “নির্দোষ” দেখাতে পারে, শুধু ভুল ধরনের profiling ব্যবহারের কারণে।

নিয়ম: perf একটা অত্যন্ত নির্ভরযোগ্য কিন্তু “perfect ground truth” না — বড় percentage পার্থক্য (৪৫% বনাম ১১%) নিয়ে নিশ্চিন্তে সিদ্ধান্ত নেওয়া যায়, কিন্তু 23.1% বনাম 24.3%-এর মতো ছোট পার্থক্যকে অতিরিক্ত গুরুত্ব দেওয়া উচিত না — এটাই পরের লেসনের “একটা সংখ্যা প্রমাণ করে না” নীতির পূর্বাভাস।

“একটা function-এর CPU সময় কম মানে সেটা performance সমস্যার কারণ হতে পারে না।”

Off-CPU time-এর আলোচনা থেকে সরাসরি একটা বিপজ্জনক ভুল সিদ্ধান্ত বেরিয়ে আসতে পারে এটাই।

একটা function যেখানে thread একটা mutex-এর জন্য 50ms ব্লকড থাকে, সেই ৫০ মিলিসেকেন্ডে থ্রেড-টা কোনো CPU cycle ব্যবহার করছে না — তাই perf record-এর (on-CPU) সংখ্যায় সেই function-এর “CPU সময়” প্রায় শূন্য দেখাবে। কেউ যদি সিদ্ধান্ত নেয় “কম CPU সময় মানে এটা bottleneck না,” সে ভুল করবে — user-এর কাছে সেই 50ms wall-clock অপেক্ষা ঠিক ততটাই বাস্তব, CPU busy ছিল কি না তা তার কাছে অপ্রাসঙ্গিক।

নিয়ম: একটা latency সমস্যা তদন্তে সবসময় দুইটা প্রশ্ন আলাদাভাবে জিজ্ঞাসা করুন — “CPU কোথায় ব্যস্ত ছিল?” (on-CPU profiling) আর “thread কোথায় অপেক্ষা করছিল?” (off-CPU profiling)। শুধু প্রথমটা দেখলে lock contention, I/O wait, বা scheduler delay-জনিত সমস্যা সম্পূর্ণ অদৃশ্য থেকে যায় — এটাই পরের লেসনের wall-clock বনাম CPU time বিভ্রান্তির profiling-স্তরের প্রতিরূপ।

বুঝেছেন কি না দেখুন

1

perf stat-এ IPC 0.35 দেখাচ্ছে, আর cache-miss rate 68%। একই সময়ে branch-miss rate মাত্র 1.2%। প্রোগ্রামটা কী ধরনের বাধায় আটকে আছে বলে মনে হয়, আর কেন?

যুক্তি

Memory-bound — নিম্ন IPC আর উচ্চ cache-miss rate একসাথে খুব স্পষ্ট signature।

IPC = 0.35 মানে গড়ে প্রতি প্রায় ৩ cycle-এ ১টা instruction retire হচ্ছে — একটা modern superscalar CPU-র সম্ভাব্য (৪+) থেকে অনেক নিচে। কিন্তু branch-miss rate কম (1.2%), তাই সমস্যা speculative execution/misprediction না।

68% cache-miss rate অত্যন্ত উচ্চ — প্রতি তিনটা memory access-এর দুইটার বেশি DRAM পর্যন্ত যাচ্ছে, প্রতিটাই ~২০০ cycle অপেক্ষা। CPU pipeline সেই সময় বসে থাকছে (memory stall) — এটাই নিচু IPC-এর মূল কারণ।

Roofline ভাষায়: এটা একটা কম arithmetic intensity-র kernel-এর lakষণ — গত লেসনের SAXPY-এর মতো। সমাধান হবে cache blocking/tiling, data layout পুনর্বিন্যাস, বা prefetching — SIMD/branch prediction টিউনিং না, কারণ সেগুলো এই bottleneck-এ কোনো প্রভাব ফেলবে না।

2

perf record আর perf stat-এর মধ্যে মৌলিক পার্থক্য কী — একটা বাক্যে?

স্মরণ

perf stat পুরো run জুড়ে counter যোগ করে একটা aggregate সংখ্যা দেয় (“মোট কতগুলো cache miss হলো”), আর perf record পর্যায়ক্রমিক sample নেয় (“প্রতিটা sample-এর মুহূর্তে কোন function executing ছিল”), যা থেকে সময় কোথায় গেল তার একটা distribution তৈরি হয়।

সহজ উপমা: perf stat একটা trip-এর মোট distance/fuel বলে; perf record প্রতি ৫ মিনিটে GPS location snapshot নিয়ে বলে দেয় ট্রিপের কোন অংশে সবচেয়ে বেশি সময় কেটেছে।

3

একটা flamegraph-এ parse_json নামের একটা বাক্স main-এর অনেক উপরে, আর তার বাম পাশে render_html নামের আরেকটা বাক্স আছে, প্রায় সমান প্রস্থের। এর মানে কি parse_json render_html-এর আগে চলেছে?

প্রয়োগ

না, এই সিদ্ধান্তটা ভুল। Flamegraph-এ বাম-ডান অবস্থান সাধারণত বাক্সগুলোকে alphabetically সাজানো হয় (readability-র জন্য), সময়ের ক্রম অনুযায়ী না।

যা সঠিকভাবে বলা যায় তা হলো — দুইটা বাক্সের প্রস্থ প্রায় সমান, অর্থাৎ দুইটা function মোট sample-এর প্রায় সমান অংশ দখল করেছে (প্রায় সমান পরিমাণ CPU সময় খেয়েছে, সব call মিলিয়ে)। তাদের আপেক্ষিক বাম-ডান অবস্থান থেকে কোনটা “আগে” বা “পরে” ঘটেছে তা বলা যায় না।

সময়ক্রম জানতে হলে দরকার একটা ভিন্ন visualization — যেমন একটা timeline/Gantt-style trace view, যেখানে x-axis সত্যিই সময় বোঝায়। Flamegraph-এর x-axis শুধু “কতটুকু” বোঝায়, “কখন” না।

4

gcc -O2 দিয়ে compile করা একটা প্রোগ্রামে perf record -g চালিয়ে perf report দেখলে পুরো call graph একটামাত্র flat frame-এ ধসে পড়েছে — কোনো caller/callee সম্পর্ক দেখা যাচ্ছে না। সম্ভাব্য কারণ ও সমাধান কী?

যুক্তি

সম্ভাব্য কারণ frame pointer omission। -O2-এ GCC ডিফল্টভাবে rbp register-কে সাধারণ general-purpose register হিসেবে পুনর্ব্যবহার করে (extra register পেলে কিছু optimization সহজ হয়) — কিন্তু এই একই register-টাই frame-pointer-based stack unwinding-এর জন্য দরকার।

rbp না থাকলে perf-এর default (frame-pointer) unwinder জানে না কীভাবে stack-এর মধ্য দিয়ে “উপরে” উঠে caller খুঁজবে — ফলে শুধু current instruction pointer-টুকুই ধরা পড়ে, পুরো chain না।

দুইটা সমাধান:

১. Compile করার সময় frame pointer explicitly রাখা:

gcc -O2 -fno-omit-frame-pointer -g -o prog prog.c

২. অথবা compile flag না বদলে DWARF বা LBR-ভিত্তিক unwinding ব্যবহার করা:

perf record --call-graph dwarf -o perf.data ./prog
# অথবা, Intel CPU-তে LBR সমর্থিত হলে:
perf record --call-graph lbr -o perf.data ./prog

Production-এ প্রথমটা (-fno-omit-frame-pointer) বেশি ব্যবহৃত হয় কারণ এর overhead প্রায় শূন্য আর সবসময় নির্ভরযোগ্য — এই কারণেই অনেক বড় কোম্পানি (Google সহ) তাদের পুরো production build-এ এই flag সবসময় চালু রাখে, শুধু profiling সহজ করার জন্য।

5

আপনি একটা production database সার্ভারে একটা periodic latency spike তদন্ত করছেন, যা মাত্র কয়েক সেকেন্ড স্থায়ী হয়, দিনে ২-৩ বার ঘটে। Continuous sampling profiler (perf record সারাদিন চালিয়ে রাখা) ব্যবহার করবেন, নাকি অন্য কোনো কৌশল? Trade-off আলোচনা করুন।

ডিজাইন

সরাসরি সারাদিন perf record -g চালিয়ে রাখা কাজ করবে, কিন্তু কয়েকটা ব্যবহারিক সমস্যা আছে:

সমস্যা ১ — data আকার। ২৪ ঘণ্টা continuous high-rate sampling বিশাল perf.data ফাইল তৈরি করবে (কয়েক GB), যার বেশিরভাগই “স্বাভাবিক” আচরণ — spike-এর মুহূর্তটুকু একটা ছোট্ট অংশ মাত্র।

সমস্যা ২ — overhead সঞ্চয়। যদিও per-sample overhead কম, সারাদিন ধরে চালানো cumulative overhead আর disk I/O সংশ্লিষ্ট সমস্যা তৈরি করতে পারে সার্ভারের normal load-এর উপর।

ভালো কৌশল — trigger-ভিত্তিক capture:

১. একটা lightweight monitoring script latency metric watch করবে (application-level histogram বা perf stat-এর মতো low-overhead continuous counter)। ২. Latency spike detect হওয়ার সাথে সাথে script স্বয়ংক্রিয়ভাবে একটা সংক্ষিপ্ত (৫-১০ সেকেন্ড) perf record চালাবে (timeout 10 perf record -g -p <pid>)। ৩. শুধু সেই capture window-এর data বিশ্লেষণ করা হবে — বাকি সময় কোনো ভারী profiling overhead নেই।

বিকল্প — সবসময়-চালু কিন্তু খুব কম rate-এ continuous profiling (এই লেসনের realworld অংশে উল্লেখিত Pyroscope/Parca-এর মতো টুল) — এরা এত কম sample rate-এ চালায় (overhead < ১%) যে সারাক্ষণ চালিয়ে রাখা নিরাপদ, আর spike-এর সময়ের data পরে retroactively বিশ্লেষণ করা যায়।

সিদ্ধান্তের মূলনীতি: rare, unpredictable event-এর জন্য হয় trigger-ভিত্তিক targeted capture, নয়তো এত কম overhead-এর continuous profiling ব্যবহার করুন যা সবসময় চালিয়ে রাখা নিরাপদ — একটা ভারী, সারাক্ষণ চালানো high-rate profiler কখনোই সঠিক পছন্দ না।

6

একটা multi-threaded web server-এ perf record -g চালিয়ে দেখলেন CPU সময়ের 95% একটা epoll_wait-সংক্রান্ত kernel function-এ যাচ্ছে, বাকি 5% application logic-এ। কিন্তু server-এর latency তবু খারাপ। কী ঘটছে বলে মনে হয়, আর পরের ধাপে কী করবেন?

প্রয়োগ

প্রথমে লক্ষ্য করুন — epoll_wait নিজেই একটা blocking syscall, যা thread-কে ঘুম পাড়িয়ে রাখে যতক্ষণ না নতুন কোনো network event আসে। এই function-এ “৯৫% সময়” দেখানো আসলে বলছে থ্রেড-টা বেশিরভাগ সময় idle, নতুন request-এর জন্য অপেক্ষায় — এটা প্রায়ই একটা স্বাভাবিক, স্বাস্থ্যকর signature একটা low-traffic বা well-provisioned server-এর জন্য, কোনো সমস্যা না।

তাই যদি latency তবু খারাপ, perf record-এর এই on-CPU সংখ্যা ভুল প্রশ্নের উত্তর দিচ্ছে। সমস্যাটা সম্ভবত এমন কোথাও যেখানে CPU busy ছিল না কিন্তু request তবু আটকে ছিল — যেমন একটা downstream database call-এর জন্য অপেক্ষা, বা একটা lock-এর জন্য অপেক্ষা যা epoll_wait-এর বাইরে ঘটছে।

পরের ধাপ — off-CPU profiling:

perf sched record -- sleep 30
perf sched latency

অথবা eBPF-ভিত্তিক offcputime টুল দিয়ে সরাসরি দেখুন কোন stack, কতক্ষণ ব্লকড ছিল — CPU busy ছিল কি না তা উপেক্ষা করে, শুধু wall-clock wait time ভিত্তিক।

মূল শিক্ষা: perf record-এর ডিফল্ট (on-CPU) mode একা একটা I/O-bound বা lock-bound সার্ভারের প্রকৃত bottleneck দেখাতে পারে না — এটা এই লেসনের hood section-এ আলোচিত blind spot-এর একটা সরাসরি বাস্তব ঘটনা। “সবচেয়ে বেশি % দেখানো function”-ই সবসময় সমস্যা না, বিশেষত যখন সেটা নিজেই একটা blocking/waiting primitive।

এরপর কী

এরপর — একটা সংখ্যা পাওয়া, আর সেই সংখ্যাকে বিশ্বাস করা

এই লেসনে আমরা শিখেছি কীভাবে একটা measurement নেওয়া যায় — perf stat দিয়ে একটা aggregate সংখ্যা, perf record+flamegraph দিয়ে একটা hotspot। কিন্তু একটা প্রশ্ন এখনো অনুত্তরিত: সেই একটা সংখ্যা কি বিশ্বাসযোগ্য?

CPU frequency scaling, cache warm-up, অন্য প্রসেসের সাথে প্রতিযোগিতা, এমনকি compiler যদি পুরো benchmark loop-টাই dead code হিসেবে বাদ দিয়ে দেয় — এসব প্রতিটাই একটা single measurement-কে বিভ্রান্তিকর করে দিতে পারে, perf-এর সংখ্যা যতই নির্ভুল হোক।

পরের ও এই module-এর শেষ লেসনে আমরা শিখব benchmarking methodology — কীভাবে বহুবার run করে, noise থেকে signal আলাদা করে, আর একটা প্রশ্নের সবচেয়ে বিপজ্জনক উত্তর (“compiler পুরো loop-টাই মুছে দিয়েছে, তবু আপনি ভাবছেন আপনি কিছু মেপেছেন”) এড়িয়ে একটা performance claim-কে সত্যিই প্রতিরক্ষাযোগ্য করে তোলা যায়।

আরও পড়ুন

  • Systems Performance: Enterprise and the Cloud, 2nd ed. — Brendan Gregg · perf, flamegraph, ও PMU-ভিত্তিক profiling-এর প্রামাণ্য reference
  • perf: Linux profiling with performance counters — Linux kernel documentation / perf wiki · perf-এর সরকারি ডকুমেন্টেশন ও subcommand তালিকা
  • The Flame Graph — Brendan Gregg · মূল flamegraph কৌশল প্রথম প্রকাশিত এই article-এ, Communications of the ACM, 2016