V8 Profiler 揭秘

简介:

原作者:江凌

Cpu-Profiling 概览

V8默认每毫秒采样(可配置),目前可生成Call-Tree,Tick Rank,Flame Chart, 如图:
1

线程驱动

V8内建的 Profiler 由2个线程驱动,线程名称1:V8:ProfEventProc, 线程2: V8:Profiler, 如下图:(这里以linux平台为讨论参考)

thread_class

  • 线程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平台的函数栈帧获取的原理。

464307e490adfc389ab5171024f29863

可以发现,每调用一次函数,都会对调用者的栈基址(ebp)进行压栈操作,并且由于栈基址是由当时栈顶指针(esp)而来,会发现,各层函数的栈基址很巧妙的构成了一个链,即当前的栈基址指向下一层函数栈基址所在的位置,如下图所示:

1938bcf2753d0cb67c5f98c8d61c98ed

了解了函数的调用过程,想要回溯调用栈也就很简单了,首先获取当前函数的栈基址(寄存器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_; ...}

relation

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, 实现了后续遍历。

参考

目录
相关文章
|
机器学习/深度学习 监控 Ubuntu
perf性能分析工具使用分享
perf性能分析工具使用分享
1775 0
perf性能分析工具使用分享
|
5月前
|
Linux 开发工具 Android开发
Android开发之——性能剖析器Profiler,赶紧学起来
Android开发之——性能剖析器Profiler,赶紧学起来
|
SQL 人工智能 移动开发
掌握Memory Profiler技巧:识别内存问题
Memory Profiler 是 Android Profiler 中的一个组件,可帮助您识别可能会导致应用卡顿、冻结甚至崩溃的内存泄露和内存抖动。 它显示一个应用内存使用量的实时图表,让您可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。
|
Arthas Oracle 安全
cpu分析利器 — async-profiler
async-profiler是一款采集分析java性能的工具,翻译一下github上的项目介绍
574 0
cpu分析利器 — async-profiler
|
存储 监控 Java
JVMTI 在手淘 Profiler 中的应用
JVMTI 在手淘 Profiler 中的应用
807 0
JVMTI 在手淘 Profiler 中的应用
|
分布式计算 监控 Java
Uber jvm profiler 使用
Uber jvm profiler 使用
279 0
Uber jvm profiler 使用
|
Java 图形学 异构计算
Unity - Profiler参数详解
CPU Usage ​       ● GC Alloc - 记录了游戏运行时代码产生的堆内存分配。这会导致ManagedHeap增大,加速GC的到来。我们要尽可能避免不必要的堆内存分配,同时注意:1、检测任何一次性内存分配大于2KB的选项;2、检测每帧都具有20B以上内存分配的选项。
2408 0
|
数据采集 Linux 索引
JVM Profiler IOProfiler
开篇  IOProfiler因为采集方法的限制,目前支持linux系统指标采集,但是不支持mac,windows等操作系统。  IOProfiler通过读取linux系统的/proc/self/io的当前线程的IO指标数据,该文件的内容如下图所示,通过解析成kv键值对完成采集。
1027 0
|
Java
JVM Profiler StacktraceCollectorProfiler
开篇  StacktraceCollectorProfiler主要用来采集线程的调用栈,原理是通过ManagementFactory.getThreadMXBean()返回的ThreadMXBean对象来实现。
813 0
|
Java
JVM Profiler CpuAndMemoryProfiler
开篇   CpuAndMemoryProfiler主要用来采集cpu和memory相关的信息,采集核心方法都是由ManagementFactory提供的接口: getClassLoadingMXBean() 返回 Java 虚拟机的类加载系统的管理 Bean。
889 0