天天看點

V8 Profiler 揭秘

原作者:江淩

Cpu-Profiling 概覽

V8預設每毫秒采樣(可配置),目前可生成Call-Tree,Tick Rank,Flame Chart, 如圖:

線程驅動

V8内建的 Profiler 由2個線程驅動,線程名稱1:V8:ProfEventProc, 線程2: V8:Profiler, 如下圖:(這裡以linux平台為讨論參考)

  • 線程1,主要是發送信号到主線程,處理采樣結果,加入到Call-Tree;
  • 線程2,主要負責采樣資料的持久化。
  • 線程1,2分離,主要是避免持久化的IO過程對采樣分析線程的影響。
  • Tick Event 由1MB緩存的循環隊列ticks_buffer_ 維護采樣集合。
  • Code Event 由無鎖隊列[1] events_buffer_維護内建事件采樣。

信号處理

void SignalHandler::HandleProfilerSignal(int signal, siginfo_t* info,
                                         void* context) {
  USE(info);
  if (signal != SIGPROF) return;
  Isolate* isolate = Isolate::UnsafeCurrent();
  if (isolate == NULL || !isolate->IsInUse()) {
    // We require a fully initialized and entered isolate.
    return;
  }
  if (v8::Locker::IsActive() &&
      !isolate->thread_manager()->IsLockedByCurrentThread()) {
    return;
  }
  ....
  sampler->SampleStack(state); //記錄函數棧           

信号處理函數異常處理包括:1)SIGPROF 過濾;2)isolate 是否完全初始化校驗;3)自鎖的情況判斷。

處理完上述情況後,就是提取

context

并記錄。

函數棧幀

void ProfileGenerator::RecordTickSample(const TickSample& sample) {
  // Allocate space for stack frames + pc + function + vm-state.
  ScopedVector<CodeEntry*> entries(sample.frames_count + 3);
}
           

這裡記錄函數棧,pc 指針等。

下面我們一起來看下IA32平台的函數棧幀擷取的原理。

可以發現,每調用一次函數,都會對調用者的棧基址(ebp)進行壓棧操作,并且由于棧基址是由當時棧頂指針(esp)而來,會發現,各層函數的棧基址很巧妙的構成了一個鍊,即目前的棧基址指向下一層函數棧基址所在的位置,如下圖所示:

了解了函數的調用過程,想要回溯調用棧也就很簡單了,首先擷取目前函數的棧基址(寄存器ebp)的值,然後擷取該位址所指向的棧的值,該值也就是下層函數的棧基址,找到下層函數的棧基址後,重複剛才的動作,即可以将每一層函數的棧基址都找出來,這也就是我們所需要的調用棧了。

V8 對函數棧幀做了一層封裝,并細化了各種幀,StackFrame、EntryFrame、ExitFrame、StandardFrame 等等,詳見

frame.cc

, 這裡暫不做分析。

節點關系

-- 'CodeMap' { CodeTree tree_; int next_shared_id_; }
-- 'CodeEntry' {LogEventsAndTags tag_ ; Name builtin_id_ ; int shared_id_; ...}
-- 'ProfileNode' {ProfileTree* tree_; CodeEntry* entry_; unsigned self_ticks_; HashMap children_; 
                  List<ProfileNode*> children_list_; unsigned id_;}
-- 'ProfileTree' { CodeEntry root_entry_; unsigned next_node_id_; ProfileNode* root_;}
-- 'CpuProfile' { List<ProfileNode*> samples_; ProfileTree top_down_; Time start_time_; Time end_time_;}
-- 'ProfileGenerator' {CodeMap code_map_; CpuProfilesCollection* profiles_; CodeEntry* program_entry_; ...}           

CodeTree由一顆伸展樹[3]組織起來, 而相對應的内部一一對應的ProfileNode由HashMap映射,ProfileTree自身有連結清單組織串聯。

| root_  |
                      /      \     \           \
                     | child1 | child2 | ... | childn |
                 /        |       |   \
           |child1|...|childn|  |child1|...|childn|  ....
           

後序周遊實作分析

class Position {
 public:
  explicit Position(ProfileNode* node)
      : node(node), child_idx_(0) { }
  INLINE(ProfileNode* current_child()) {
    return node->children()->at(child_idx_);
  }
  INLINE(bool has_current_child()) {
    return child_idx_ < node->children()->length();
  }
  INLINE(void next_child()) { ++child_idx_; }

  ProfileNode* node; // 子節點, 它的子節點用List<ProfileNode*> children_list_;存儲
 private:
  int child_idx_; // List的疊代器
};           
// Non-recursive implementation of a depth-first post-order tree traversal.
// 非遞歸版本的深度後序周遊實作
template <typename Callback>
void ProfileTree::TraverseDepthFirst(Callback* callback) {
  List<Position> stack(10); // 初始大小為10
  stack.Add(Position(root_)); // 加入根節點
  while (stack.length() > 0) { 
    Position& current = stack.last(); // 取出棧頂元素 | root_(底) | root_left |root_left_left | ....| 頂|
    if (current.has_current_child()) { // 存在子節點
      callback->BeforeTraversingChild(current.node, current.current_child());
      stack.Add(Position(current.current_child())); // 加入子節點,直到全部加入
    } else {
      callback->AfterAllChildrenTraversed(current.node); // callback, delete node
      if (stack.length() > 1) {
        Position& parent = stack[stack.length() - 2]; // 取出父節點
        callback->AfterChildTraversed(parent.node, current.node); 
        parent.next_child(); // 注意:parent是引用,會改變child_idx_的值
      }
      // Remove child from the stack.
      stack.RemoveLast(); // 移出棧頂
    }
  }
}           

值得注意的是:由于資料結構的組織,無法實作中序周遊。

Binary Tree :
           0
         /   \
        1     4
      /   \
     2     3

* step0 : stack | #0 |
* step1 : stack | #0 | #1 | #2 |  // Add node
* step2 : stack | #0 | #1 |       // Traversed node#2 -->|@2
* step3 : stack | #0 | #1 | #3 |  // Add right node#3    
* step4 : stack | #0 | #1 |       // Traversed node#3 -->|@3 
* step5 : stack | #0 |            // Traversed node#1 -->|@1 
* step6 : stack | #0 | #4         // Add right node#4
* step7 : stack | #0 |            // Traversed node#1 -->|@4 
* step8 : stack |                 // Traversed node#1 -->|@0
總的來說: 2->3->1->4->0, 實作了後續周遊。           

參考

繼續閱讀