# class RubyProf::CallStackPrinter

prints a HTML visualization of the call tree

### Public Instance Methods

application() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 162
    def application
      @options[:application] || $PROGRAM_NAME
    end

arguments() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 166
    def arguments
      ARGV.join(' ')
    end

color(p) click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 149
    def color(p)
      case i = p.to_i
      when 0..5
        "01"
      when 5..10
        "05"
      when 100
        "9"
      else
        "#{i/10}"
      end
    end

copy_image_files() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 182
    def copy_image_files
      if @output.is_a?(File)
        target_dir = File.dirname(@output.path)
        image_dir = File.join(File.dirname(__FILE__), '..', 'images')
        %w(empty plus minus).each do |img|
          source_file = "#{image_dir}/#{img}.png"
          target_file = "#{target_dir}/#{img}.png"
          FileUtils.cp(source_file, target_file) unless File.exist?(target_file)
        end
      end
    end

dump(ci) click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 145
    def dump(ci)
      $stderr.printf "%s/%d t:%f s:%f w:%f  \n", ci, ci.object_id, ci.total_time, ci.self_time, ci.wait_time
    end

expansion() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 178
    def expansion
      @options[:expansion] || 10.0
    end

graph_link(call_info) click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 126
    def graph_link(call_info)
      total_calls = call_info.target.call_infos.inject(0){|t, ci| t += ci.called}
      href = "#{@graph_html}##{method_href(call_info.target)}"
      totals = @graph_html ? "<a href='#{href}'>#{total_calls}</a>" : total_calls.to_s
      "[#{call_info.called} calls, #{totals} total]"
    end

link(call_info) click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 112
    def link(call_info)
      method = call_info.target
      file = File.expand_path(method.source_file)
      if file =~ /\/ruby_runtime$/
        h(name(call_info))
      else
        if RUBY_PLATFORM =~ /darwin/
          "<a href=\"txmt://open?url=file://#{file}&line=#{method.line}\">#{h(name(call_info))}</a>"
        else
          "<a href=\"file://#{file}##{method.line}\">#{h(name(call_info))}</a>"
        end
      end
    end

method_href(method) click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 133
    def method_href(method)
      h(method.full_name.gsub(/[><#\.\?=:]/,"_") + "_" + @current_thread_id.to_s)
    end

name(call_info) click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 107
    def name(call_info)
      method = call_info.target
      method.full_name
    end

print(output = STDOUT, options = {}) click to toggle source

Specify print options.

options - Hash table

    :min_percent - Number 0 to 100 that specifes the minimum
                   %self (the methods self time divided by the
                   overall total time) that a method must take
                   for it to be printed out in the report.
                   Default value is 0.

    :print_file  - True or false. Specifies if a method's source
                   file should be printed.  Default value if false.

    :threshold   - a float from 0 to 100 that sets the threshold of
                   results displayed.
                   Default value is 1.0

    :title       - a String to overide the default "ruby-prof call tree"
                   title of the report.

    :expansion   - a float from 0 to 100 that sets the threshold of
                   results that are expanded, if the percent_total
                   exceeds it.
                   Default value is 10.0

    :application - a String to overide the name of the application,
                   as it appears on the report.

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 37
    def print(output = STDOUT, options = {})
      @output = output
      setup_options(options)
      if @graph_html = options.delete(:graph)
        @graph_html = "file://" + @graph_html if @graph_html[0]=="/"
      end

      print_header

      @overall_threads_time = @result.threads.inject(0) do |val, thread|
        val += thread.total_time
      end

      @result.threads.each do |thread|
        @current_thread_id = thread.fiber_id
        @overall_time = thread.total_time
        thread_info = "Thread: #{thread.id}"
        thread_info << ", Fiber: #{thread.fiber_id}" unless thread.id == thread.fiber_id
        thread_info << " (#{"%4.2f%%" % ((@overall_time/@overall_threads_time)*100)} ~ #{@overall_time})"
        @output.print "<div class=\"thread\">#{thread_info}</div>"
        @output.print "<ul name=\"thread\">"
        thread.methods.each do |m|
          # $stderr.print m.dump
          next unless m.root?
          m.call_infos.each do |ci|
            next unless ci.root?
            print_stack ci, thread.total_time
          end
        end
        @output.print "</ul>"
      end

      print_footer

      copy_image_files
    end

print_commands() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 740
        def print_commands
          @output.puts <<-"end_commands"
    <div id=\"commands\">
    <span style=\"font-size: 11pt; font-weight: bold;\">Threshold:</span>
    <input value=\"#{h threshold}\" size=\"3\" id=\"threshold\" type=\"text\">
    <input value=\"Apply\" onclick=\"setThreshold();\" type=\"submit\">
    <input value=\"Expand All\" onclick=\"expandAll(event);\" type=\"submit\">
    <input value=\"Collapse All\" onclick=\"collapseAll(event);\" type=\"submit\">
    <input value=\"Show Help\" onclick=\"toggleHelp(this);\" type=\"submit\">
    </div>
    end_commands
        end

print_css() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 210
        def print_css
          @output.puts <<-'end_css'
    <style type="text/css">
    <!--
    body {
        font-size:70%;
        padding:0px;
        margin:5px;
        margin-right:0px;
        margin-left:0px;
        background: #ffffff;
    }
    ul {
        margin-left:0px;
        margin-top:0px;
        margin-bottom:0px;
        padding-left:0px;
        list-style-type:none;
    }
    li {
        margin-left:11px;
        padding:0px;
        white-space:nowrap;
        border-top:1px solid #cccccc;
        border-left:1px solid #cccccc;
        border-bottom:none;
    }
    .thread {
        margin-left:11px;
        background:#708090;
        padding-top:3px;
        padding-left:12px;
        padding-bottom:2px;
        border-left:1px solid #CCCCCC;
        border-top:1px solid #CCCCCC;
        font-weight:bold;
    }
    .hidden {
        display:none;
        width:0px;
        height:0px;
        margin:0px;
        padding:0px;
        border-style:none;
    }
    .color01 { background:#adbdeb }
    .color05 { background:#9daddb }
    .color0 { background:#8d9dcb }
    .color1 { background:#89bccb }
    .color2 { background:#56e3e7 }
    .color3 { background:#32cd70 }
    .color4 { background:#a3d53c }
    .color5 { background:#c4cb34 }
    .color6 { background:#dcb66d }
    .color7 { background:#cda59e }
    .color8 { background:#be9d9c }
    .color9 { background:#cf947a }
    #commands {
        font-size:10pt;
        padding:10px;
        margin-left:11px;
        margin-bottom:0px;
        margin-top:0px;
        background:#708090;
        border-top:1px solid #cccccc;
        border-left:1px solid #cccccc;
        border-bottom:none;
    }
    #titlebar {
        font-size:10pt;
        padding:10px;
        margin-left:11px;
        margin-bottom:0px;
        margin-top:10px;
        background:#8090a0;
        border-top:1px solid #cccccc;
        border-left:1px solid #cccccc;
        border-bottom:none;
    }
    #help {
        font-size:10pt;
        padding:10px;
        margin-left:11px;
        margin-bottom:0px;
        margin-top:0px;
        background:#8090a0;
        display:none;
        border-top:1px solid #cccccc;
        border-left:1px solid #cccccc;
        border-bottom:none;
    }
    #sentinel {
        height: 400px;
        margin-left:11px;
        background:#8090a0;
        border-top:1px solid #cccccc;
        border-left:1px solid #cccccc;
        border-bottom:none;
     }
    input { margin-left:10px; }
    -->
    </style>
    end_css
        end

print_header() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 194
    def print_header
      @output.puts "<html><head>"
      @output.puts '<meta http-equiv="content-type" content="text/html; charset=utf-8">'
      @output.puts "<title>#{h title}</title>"
      print_css
      print_java_script
      @output.puts '</head><body><div style="display: inline-block;">'
      print_title_bar
      print_commands
      print_help
    end

print_help() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 753
        def print_help
          @output.puts <<-'end_help'
    <div style="display: none;" id="help">
    <img src="empty.png"> Enter a decimal value <i>d</i> into the threshold field and click "Apply"
    to hide all nodes marked with time values lower than <i>d</i>.<br>
    <img src="empty.png"> Click on "Expand All" for full tree expansion.<br>
    <img src="empty.png"> Click on "Collapse All" to show only top level nodes.<br>
    <img src="empty.png"> Use a, s, d, w as in Quake or Urban Terror to navigate the tree.<br>
    <img src="empty.png"> Use f and b to navigate the tree in preorder forward and backwards.<br>
    <img src="empty.png"> Use x to toggle visibility of a subtree.<br>
    <img src="empty.png"> Use * to expand/collapse a whole subtree.<br>
    <img src="empty.png"> Use h to navigate to thread root.<br>
    <img src="empty.png"> Use n and p to navigate between threads.<br>
    <img src="empty.png"> Click on background to move focus to a subtree.<br>
    </div>
    end_help
        end

print_java_script() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 315
        def print_java_script
          @output.puts <<-'end_java_script'
    <script type="text/javascript">
    /*
       Copyright (C) 2005,2009  Stefan Kaes
       skaes@railsexpress.de
    */

    function rootNode() {
      return currentThread;
    }

    function hideUL(node) {
      var lis = node.childNodes
      var l = lis.length;
      for (var i=0; i < l ; i++ ) {
        hideLI(lis[i]);
      }
    }

    function showUL(node) {
      var lis = node.childNodes;
      var l = lis.length;
      for (var i=0; i < l ; i++ ) {
        showLI(lis[i]);
      }
    }

    function findUlChild(li){
      var ul = li.childNodes[2];
      while (ul && ul.nodeName != "UL") {
        ul = ul.nextSibling;
      }
      return ul;
    }

    function isLeafNode(li) {
      var img = li.firstChild;
      return (img.src.indexOf('empty.png') > -1);
    }

    function hideLI(li) {
      if (isLeafNode(li))
        return;

      var img = li.firstChild;
      img.src = 'plus.png';

      var ul = findUlChild(li);
      if (ul) {
        ul.style.display = 'none';
        hideUL(ul);
      }
    }

    function showLI(li) {
      if (isLeafNode(li))
        return;

      var img = li.firstChild;
      img.src = 'minus.png';

      var ul = findUlChild(li);
      if (ul) {
        ul.style.display = 'block';
        showUL(ul);
      }
    }

    function toggleLI(li) {
      var img = li.firstChild;
      if (img.src.indexOf("minus.png")>-1)
        hideLI(li);
      else {
        if (img.src.indexOf("plus.png")>-1)
          showLI(li);
      }
    }

    function aboveThreshold(text, threshold) {
      var match = text.match(/\d+[.,]\d+/);
      return (match && parseFloat(match[0].replace(/,/, '.'))>=threshold);
    }

    function setThresholdLI(li, threshold) {
      var img = li.firstChild;
      var text = img.nextSibling;
      var ul = findUlChild(li);

      var visible = aboveThreshold(text.nodeValue, threshold) ? 1 : 0;

      var count = 0;
      if (ul) {
        count = setThresholdUL(ul, threshold);
      }
      if (count>0) {
        img.src = 'minus.png';
      }
      else {
        img.src = 'empty.png';
      }
      if (visible) {
        li.style.display = 'block'
      }
      else {
        li.style.display = 'none'
      }
      return visible;
    }

    function setThresholdUL(node, threshold) {
      var lis = node.childNodes;
      var l = lis.length;

      var count = 0;
      for ( var i = 0; i < l ; i++ ) {
        count = count + setThresholdLI(lis[i], threshold);
      }

      var visible = (count > 0) ? 1 : 0;
      if (visible) {
        node.style.display = 'block';
      }
      else {
        node.style.display = 'none';
      }
      return visible;
    }

    function toggleChildren(img, event) {
      event.cancelBubble=true;

      if (img.src.indexOf('empty.png') > -1)
        return;

      var minus = (img.src.indexOf('minus.png') > -1);

      if (minus) {
        img.src = 'plus.png';
      }
      else
        img.src = 'minus.png';

      var li = img.parentNode;
      var ul = findUlChild(li);
      if (ul) {
        if (minus)
          ul.style.display = 'none';
        else
          ul.style.display = 'block';
      }
      if (minus)
        moveSelectionIfNecessary(li);
    }

    function showChildren(li) {
      var img = li.firstChild;
      if (img.src.indexOf('empty.png') > -1)
        return;
      img.src = 'minus.png';

      var ul = findUlChild(li);
      if (ul) {
        ul.style.display = 'block';
      }
    }

    function setThreshold() {
     var tv = document.getElementById("threshold").value;
     if (tv.match(/[0-9]+([.,][0-9]+)?/)) {
       var f = parseFloat(tv.replace(/,/, '.'));
       var threads = document.getElementsByName("thread");
       var l = threads.length;
       for ( var i = 0; i < l ; i++ ) {
         setThresholdUL(threads[i], f);
       }
       var p = selectedNode;
       while (p && p.style.display=='none')
         p=p.parentNode.parentNode;
       if (p && p.nodeName=="LI")
        selectNode(p);
     }
     else {
       alert("Please specify a decimal number as threshold value!");
     }
    }

    function collapseAll(event) {
      event.cancelBubble=true;
      var threads = document.getElementsByName("thread");
      var l = threads.length;
      for ( var i = 0; i < l ; i++ ) {
        hideUL(threads[i]);
      }
      selectNode(rootNode(), null);
    }

    function expandAll(event) {
      event.cancelBubble=true;
      var threads = document.getElementsByName("thread");
      var l = threads.length;
      for ( var i = 0; i < l ; i++ ) {
        showUL(threads[i]);
      }
    }

    function toggleHelp(node) {
      var help = document.getElementById("help");
      if (node.value == "Show Help") {
        node.value = "Hide Help";
        help.style.display = 'block';
      }
      else {
        node.value = "Show Help";
        help.style.display = 'none';
      }
    }

    var selectedNode = null;
    var selectedColor = null;
    var selectedThread = null;

    function descendentOf(a,b){
      while (a!=b && b!=null)
        b=b.parentNode;
      return (a==b);
    }

    function moveSelectionIfNecessary(node){
      if (descendentOf(node, selectedNode))
        selectNode(node, null);
    }

    function selectNode(node, event) {
      if (event) {
        event.cancelBubble = true;
        thread = findThread(node);
        selectThread(thread);
      }
      if (selectedNode) {
        selectedNode.style.background = selectedColor;
      }
      selectedNode = node;
      selectedColor = node.style.background;
      selectedNode.style.background = "red";
      selectedNode.scrollIntoView();
      window.scrollBy(0,-400);
    }

    function moveUp(){
      var p = selectedNode.previousSibling;
      while (p && p.style.display == 'none')
        p = p.previousSibling;
      if (p && p.nodeName == "LI") {
        selectNode(p, null);
      }
    }

    function moveDown(){
      var p = selectedNode.nextSibling;
      while (p && p.style.display == 'none')
        p = p.nextSibling;
      if (p && p.nodeName == "LI") {
        selectNode(p, null);
      }
    }

    function moveLeft(){
      var p = selectedNode.parentNode.parentNode;
      if (p && p.nodeName=="LI") {
        selectNode(p, null);
      }
    }

    function moveRight(){
      if (!isLeafNode(selectedNode)) {
        showChildren(selectedNode);
        var ul = findUlChild(selectedNode);
        if (ul) {
          selectNode(ul.firstChild, null);
        }
      }
    }

    function moveForward(){
      if (isLeafNode(selectedNode)) {
        var p = selectedNode;
        while ((p.nextSibling == null || p.nextSibling.style.display=='none') && p.nodeName=="LI") {
          p = p.parentNode.parentNode;
        }
        if (p.nodeName=="LI")
          selectNode(p.nextSibling, null);
      }
      else {
        moveRight();
      }
    }

    function isExpandedNode(li){
      var img = li.firstChild;
      return(img.src.indexOf('minus.png')>-1);
    }

    function moveBackward(){
      var p = selectedNode;
      var q = p.previousSibling;
      while (q != null && q.style.display=='none')
        q = q.previousSibling;
      if (q == null) {
        p = p.parentNode.parentNode;
      } else {
        while (!isLeafNode(q) && isExpandedNode(q)) {
          q = findUlChild(q).lastChild;
          while (q.style.display=='none')
            q = q.previousSibling;
        }
        p = q;
      }
      if (p.nodeName=="LI")
        selectNode(p, null);
    }

    function moveHome() {
      selectNode(currentThread);
    }

    var currentThreadIndex = null;

    function findThread(node){
      while (node && node.parentNode.nodeName!="BODY") {
        node = node.parentNode;
      }
      return node.firstChild;
    }

    function selectThread(node){
      var threads = document.getElementsByName("thread");
      currentThread = node;
      for (var i=0; i<threads.length; i++) {
        if (threads[i]==currentThread.parentNode)
          currentThreadIndex = i;
      }
    }

    function nextThread(){
      var threads = document.getElementsByName("thread");
      if (currentThreadIndex==threads.length-1)
        currentThreadIndex = 0;
      else
        currentThreadIndex += 1
      currentThread = threads[currentThreadIndex].firstChild;
      selectNode(currentThread, null);
    }

    function previousThread(){
      var threads = document.getElementsByName("thread");
      if (currentThreadIndex==0)
        currentThreadIndex = threads.length-1;
      else
        currentThreadIndex -= 1
      currentThread = threads[currentThreadIndex].firstChild;
      selectNode(currentThread, null);
    }

    function switchThread(node, event){
      event.cancelBubble = true;
      selectThread(node.nextSibling.firstChild);
      selectNode(currentThread, null);
    }

    function handleKeyEvent(event){
      var code = event.charCode ? event.charCode : event.keyCode;
      var str = String.fromCharCode(code);
      switch (str) {
        case "a": moveLeft();  break;
        case "s": moveDown();  break;
        case "d": moveRight(); break;
        case "w": moveUp();    break;
        case "f": moveForward(); break;
        case "b": moveBackward(); break;
        case "x": toggleChildren(selectedNode.firstChild, event); break;
        case "*": toggleLI(selectedNode); break;
        case "n": nextThread(); break;
        case "h": moveHome(); break;
        case "p": previousThread(); break;
      }
    }
    document.onkeypress=function(event){ handleKeyEvent(event) };

    window.onload=function(){
      var images = document.getElementsByTagName("img");
      for (var i=0; i<images.length; i++) {
        var img = images[i];
        if (img.className == "toggle") {
          img.onclick = function(event){ toggleChildren(this, event); };
        }
      }
      var divs = document.getElementsByTagName("div");
      for (i=0; i<divs.length; i++) {
        var div = divs[i];
        if (div.className == "thread")
          div.onclick = function(event){ switchThread(this, event) };
      }
      var lis = document.getElementsByTagName("li");
      for (var i=0; i<lis.length; i++) {
        lis[i].onclick = function(event){ selectNode(this, event); };
      }
      var threads = document.getElementsByName("thread");
      currentThreadIndex = 0;
      currentThread = threads[0].firstChild;
      selectNode(currentThread, null);
    }
    </script>
    end_java_script
        end

print_stack(call_info, parent_time) click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 74
    def print_stack(call_info, parent_time)
      total_time = call_info.total_time
      percent_parent = (total_time/parent_time)*100
      percent_total = (total_time/@overall_time)*100
      return unless percent_total > min_percent
      color = self.color(percent_total)
      kids = call_info.children
      visible = percent_total >= threshold
      expanded = percent_total >= expansion
      display = visible ? "block" : "none"
      @output.print "<li class=\"color#{color}\" style=\"display:#{display}\">"
      if kids.empty?
        @output.print "<img src=\"empty.png\">"
      else
        visible_children = kids.any?{|ci| (ci.total_time/@overall_time)*100 >= threshold}
        image = visible_children ? (expanded ? "minus" : "plus") : "empty"
        @output.print "<img class=\"toggle\" src=\"#{image}.png\">"
      end
      @output.printf " %4.2f%% (%4.2f%%) %s %s\n", percent_total, percent_parent, link(call_info), graph_link(call_info)
      unless kids.empty?
        if expanded
          @output.print "<ul>"
        else
          @output.print '<ul style="display:none">'
        end
        kids.sort_by{|c| -c.total_time}.each do |callinfo|
          print_stack callinfo, total_time
        end
        @output.print "</ul>"
      end
      @output.print "</li>"
    end

print_title_bar() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 731
        def print_title_bar
          @output.puts <<-"end_title_bar"
    <div id="titlebar">
    Call tree for application <b>#{h application} #{h arguments}</b><br/>
    Generated on #{Time.now} with options #{h @options.inspect}<br/>
    </div>
    end_title_bar
        end

sum(a) click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 141
    def sum(a)
      a.inject(0.0){|s,t| s+=t}
    end

threshold() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 174
    def threshold
      @options[:threshold] || 1.0
    end

title() click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 170
    def title
      @title ||= @options.delete(:title) || "ruby-prof call tree"
    end

total_time(call_infos) click to toggle source

    # File lib/ruby-prof/printers/call_stack_printer.rb, line 137
    def total_time(call_infos)
      sum(call_infos.map{|ci| ci.total_time})
    end