Home | History | Annotate | Download | only in profviz
      1 // Copyright 2013 the V8 project authors. All rights reserved.
      2 // Redistribution and use in source and binary forms, with or without
      3 // modification, are permitted provided that the following conditions are
      4 // met:
      5 //
      6 //     * Redistributions of source code must retain the above copyright
      7 //       notice, this list of conditions and the following disclaimer.
      8 //     * Redistributions in binary form must reproduce the above
      9 //       copyright notice, this list of conditions and the following
     10 //       disclaimer in the documentation and/or other materials provided
     11 //       with the distribution.
     12 //     * Neither the name of Google Inc. nor the names of its
     13 //       contributors may be used to endorse or promote products derived
     14 //       from this software without specific prior written permission.
     15 //
     16 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     17 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     18 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     19 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     20 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     21 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     22 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     23 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     24 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     25 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     26 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     27 
     28 Array.prototype.top = function() {
     29   if (this.length == 0) return undefined;
     30   return this[this.length - 1];
     31 }
     32 
     33 
     34 function PlotScriptComposer(kResX, kResY, error_output) {
     35   // Constants.
     36   var kV8BinarySuffixes = ["/d8", "/libv8.so"];
     37   var kStackFrames = 8;             // Stack frames to display in the plot.
     38 
     39   var kTimerEventWidth = 0.33;      // Width of each timeline.
     40   var kExecutionFrameWidth = 0.2;   // Width of the top stack frame line.
     41   var kStackFrameWidth = 0.1;       // Width of the lower stack frame lines.
     42   var kGapWidth = 0.05;             // Gap between stack frame lines.
     43 
     44   var kY1Offset = 11;               // Offset for stack frame vs. event lines.
     45   var kDeoptRow = 7;                // Row displaying deopts.
     46   var kMaxDeoptLength = 4;          // Draw size of the largest deopt.
     47   var kPauseLabelPadding = 5;       // Padding for pause time labels.
     48   var kNumPauseLabels = 7;          // Number of biggest pauses to label.
     49   var kCodeKindLabelPadding = 100;  // Padding for code kind labels.
     50 
     51   var kTickHalfDuration = 0.5;      // Duration of half a tick in ms.
     52   var kMinRangeLength = 0.0005;     // Minimum length for an event in ms.
     53 
     54   var kNumThreads = 2;              // Number of threads.
     55   var kExecutionThreadId = 0;       // ID of main thread.
     56 
     57   // Init values.
     58   var num_timer_event = kY1Offset + 0.5;
     59 
     60   // Data structures.
     61   function TimerEvent(label, color, pause, thread_id) {
     62     assert(thread_id >= 0 && thread_id < kNumThreads, "invalid thread id");
     63     this.label = label;
     64     this.color = color;
     65     this.pause = pause;
     66     this.ranges = [];
     67     this.thread_id = thread_id;
     68     this.index = ++num_timer_event;
     69   }
     70 
     71   function CodeKind(color, kinds) {
     72     this.color = color;
     73     this.in_execution = [];
     74     this.stack_frames = [];
     75     for (var i = 0; i < kStackFrames; i++) this.stack_frames.push([]);
     76     this.kinds = kinds;
     77   }
     78 
     79   function Range(start, end) {
     80     this.start = start;  // In milliseconds.
     81     this.end = end;      // In milliseconds.
     82   }
     83 
     84   function Deopt(time, size) {
     85     this.time = time;  // In milliseconds.
     86     this.size = size;  // In bytes.
     87   }
     88 
     89   Range.prototype.duration = function() { return this.end - this.start; }
     90 
     91   function Tick(tick) {
     92     this.tick = tick;
     93   }
     94 
     95   var TimerEvents = {
     96       'V8.Execute':
     97         new TimerEvent("execution", "#000000", false, 0),
     98       'V8.External':
     99         new TimerEvent("external", "#3399FF", false, 0),
    100       'V8.CompileFullCode':
    101         new TimerEvent("compile unopt", "#CC0000",  true, 0),
    102       'V8.RecompileSynchronous':
    103         new TimerEvent("recompile sync", "#CC0044",  true, 0),
    104       'V8.RecompileConcurrent':
    105         new TimerEvent("recompile async", "#CC4499", false, 1),
    106       'V8.CompileEval':
    107         new TimerEvent("compile eval", "#CC4400",  true, 0),
    108       'V8.Parse':
    109         new TimerEvent("parse", "#00CC00",  true, 0),
    110       'V8.PreParse':
    111         new TimerEvent("preparse", "#44CC00",  true, 0),
    112       'V8.ParseLazy':
    113         new TimerEvent("lazy parse", "#00CC44",  true, 0),
    114       'V8.GCScavenger':
    115         new TimerEvent("gc scavenge", "#0044CC",  true, 0),
    116       'V8.GCCompactor':
    117         new TimerEvent("gc compaction", "#4444CC",  true, 0),
    118       'V8.GCContext':
    119         new TimerEvent("gc context", "#4400CC",  true, 0),
    120   };
    121 
    122   var CodeKinds = {
    123       'external ': new CodeKind("#3399FF", [-2]),
    124       'runtime  ': new CodeKind("#000000", [-1]),
    125       'full code': new CodeKind("#DD0000", [0]),
    126       'opt code ': new CodeKind("#00EE00", [1]),
    127       'code stub': new CodeKind("#FF00FF", [2]),
    128       'built-in ': new CodeKind("#AA00AA", [3]),
    129       'inl.cache': new CodeKind("#4444AA",
    130                                 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]),
    131       'reg.exp. ': new CodeKind("#0000FF", [15]),
    132   };
    133 
    134   var code_map = new CodeMap();
    135   var execution_pauses = [];
    136   var deopts = [];
    137   var event_stack = [];
    138   var last_time_stamp = [];
    139   for (var i = 0; i < kNumThreads; i++) {
    140     event_stack[i] = [];
    141     last_time_stamp[i] = -1;
    142   }
    143 
    144   var range_start = undefined;
    145   var range_end = undefined;
    146   var obj_index = 0;
    147   var pause_tolerance = 0.005;  // Milliseconds.
    148   var distortion = 0;
    149 
    150   // Utility functions.
    151   function assert(something, message) {
    152     if (!something) {
    153       var error = new Error(message);
    154       error_output(error.stack);
    155     }
    156   }
    157 
    158   function FindCodeKind(kind) {
    159     for (name in CodeKinds) {
    160       if (CodeKinds[name].kinds.indexOf(kind) >= 0) {
    161         return CodeKinds[name];
    162       }
    163     }
    164   }
    165 
    166   function TicksToRanges(ticks) {
    167     var ranges = [];
    168     for (var i = 0; i < ticks.length; i++) {
    169       var tick = ticks[i].tick;
    170       ranges.push(
    171           new Range(tick - kTickHalfDuration, tick + kTickHalfDuration));
    172     }
    173     return ranges;
    174   }
    175 
    176   function MergeRanges(ranges) {
    177     ranges.sort(function(a, b) { return a.start - b.start; });
    178     var result = [];
    179     var j = 0;
    180     for (var i = 0; i < ranges.length; i = j) {
    181       var merge_start = ranges[i].start;
    182       if (merge_start > range_end) break;  // Out of plot range.
    183       var merge_end = ranges[i].end;
    184       for (j = i + 1; j < ranges.length; j++) {
    185         var next_range = ranges[j];
    186         // Don't merge ranges if there is no overlap (incl. merge tolerance).
    187         if (next_range.start > merge_end + pause_tolerance) break;
    188         // Merge ranges.
    189         if (next_range.end > merge_end) {  // Extend range end.
    190           merge_end = next_range.end;
    191         }
    192       }
    193       if (merge_end < range_start) continue;  // Out of plot range.
    194       if (merge_end < merge_start) continue;  // Not an actual range.
    195       result.push(new Range(merge_start, merge_end));
    196     }
    197     return result;
    198   }
    199 
    200   function RestrictRangesTo(ranges, start, end) {
    201     var result = [];
    202     for (var i = 0; i < ranges.length; i++) {
    203       if (ranges[i].start <= end && ranges[i].end >= start) {
    204         result.push(new Range(Math.max(ranges[i].start, start),
    205                               Math.min(ranges[i].end, end)));
    206       }
    207     }
    208     return result;
    209   }
    210 
    211   // Public methods.
    212   this.collectData = function(input, distortion_per_entry) {
    213 
    214     var last_timestamp = 0;
    215 
    216     // Parse functions.
    217     var parseTimeStamp = function(timestamp) {
    218       int_timestamp = parseInt(timestamp);
    219       assert(int_timestamp >= last_timestamp, "Inconsistent timestamps.");
    220       last_timestamp = int_timestamp;
    221       distortion += distortion_per_entry;
    222       return int_timestamp / 1000 - distortion;
    223     }
    224 
    225     var processTimerEventStart = function(name, start) {
    226       // Find out the thread id.
    227       var new_event = TimerEvents[name];
    228       if (new_event === undefined) return;
    229       var thread_id = new_event.thread_id;
    230 
    231       start = Math.max(last_time_stamp[thread_id] + kMinRangeLength, start);
    232 
    233       // Last event on this thread is done with the start of this event.
    234       var last_event = event_stack[thread_id].top();
    235       if (last_event !== undefined) {
    236         var new_range = new Range(last_time_stamp[thread_id], start);
    237         last_event.ranges.push(new_range);
    238       }
    239       event_stack[thread_id].push(new_event);
    240       last_time_stamp[thread_id] = start;
    241     };
    242 
    243     var processTimerEventEnd = function(name, end) {
    244       // Find out about the thread_id.
    245       var finished_event = TimerEvents[name];
    246       var thread_id = finished_event.thread_id;
    247       assert(finished_event === event_stack[thread_id].pop(),
    248              "inconsistent event stack");
    249 
    250       end = Math.max(last_time_stamp[thread_id] + kMinRangeLength, end);
    251 
    252       var new_range = new Range(last_time_stamp[thread_id], end);
    253       finished_event.ranges.push(new_range);
    254       last_time_stamp[thread_id] = end;
    255     };
    256 
    257     var processCodeCreateEvent = function(type, kind, address, size, name) {
    258       var code_entry = new CodeMap.CodeEntry(size, name);
    259       code_entry.kind = kind;
    260       code_map.addCode(address, code_entry);
    261     };
    262 
    263     var processCodeMoveEvent = function(from, to) {
    264       code_map.moveCode(from, to);
    265     };
    266 
    267     var processCodeDeleteEvent = function(address) {
    268       code_map.deleteCode(address);
    269     };
    270 
    271     var processCodeDeoptEvent = function(time, size) {
    272       deopts.push(new Deopt(time, size));
    273     }
    274 
    275     var processSharedLibrary = function(name, start, end) {
    276       var code_entry = new CodeMap.CodeEntry(end - start, name);
    277       code_entry.kind = -3;  // External code kind.
    278       for (var i = 0; i < kV8BinarySuffixes.length; i++) {
    279         var suffix = kV8BinarySuffixes[i];
    280         if (name.indexOf(suffix, name.length - suffix.length) >= 0) {
    281           code_entry.kind = -1;  // V8 runtime code kind.
    282           break;
    283         }
    284       }
    285       code_map.addLibrary(start, code_entry);
    286     };
    287 
    288     var processTickEvent = function(
    289         pc, timer, unused_x, unused_y, vmstate, stack) {
    290       var tick = new Tick(timer);
    291 
    292       var entry = code_map.findEntry(pc);
    293       if (entry) FindCodeKind(entry.kind).in_execution.push(tick);
    294 
    295       for (var i = 0; i < kStackFrames; i++) {
    296         if (!stack[i]) break;
    297         var entry = code_map.findEntry(stack[i]);
    298         if (entry) FindCodeKind(entry.kind).stack_frames[i].push(tick);
    299       }
    300     };
    301     // Collect data from log.
    302     var logreader = new LogReader(
    303       { 'timer-event-start': { parsers: [null, parseTimeStamp],
    304                                processor: processTimerEventStart },
    305         'timer-event-end':   { parsers: [null, parseTimeStamp],
    306                                processor: processTimerEventEnd },
    307         'shared-library': { parsers: [null, parseInt, parseInt],
    308                             processor: processSharedLibrary },
    309         'code-creation':  { parsers: [null, parseInt, parseInt, parseInt, null],
    310                             processor: processCodeCreateEvent },
    311         'code-move':      { parsers: [parseInt, parseInt],
    312                             processor: processCodeMoveEvent },
    313         'code-delete':    { parsers: [parseInt],
    314                             processor: processCodeDeleteEvent },
    315         'code-deopt':     { parsers: [parseTimeStamp, parseInt],
    316                             processor: processCodeDeoptEvent },
    317         'tick':           { parsers: [parseInt, parseTimeStamp,
    318                                       null, null, parseInt, 'var-args'],
    319                             processor: processTickEvent }
    320       });
    321 
    322     var line;
    323     while (line = input()) {
    324       logreader.processLogLine(line);
    325     }
    326 
    327     // Collect execution pauses.
    328     for (name in TimerEvents) {
    329       var event = TimerEvents[name];
    330       if (!event.pause) continue;
    331       var ranges = event.ranges;
    332       for (var j = 0; j < ranges.length; j++) execution_pauses.push(ranges[j]);
    333     }
    334     execution_pauses = MergeRanges(execution_pauses);
    335   };
    336 
    337 
    338   this.findPlotRange = function(
    339     range_start_override, range_end_override, result_callback) {
    340     var start_found = (range_start_override || range_start_override == 0);
    341     var end_found = (range_end_override || range_end_override == 0);
    342     range_start = start_found ? range_start_override : Infinity;
    343     range_end = end_found ? range_end_override : -Infinity;
    344 
    345     if (!start_found || !end_found) {
    346       for (name in TimerEvents) {
    347         var ranges = TimerEvents[name].ranges;
    348         for (var i = 0; i < ranges.length; i++) {
    349           if (ranges[i].start < range_start && !start_found) {
    350             range_start = ranges[i].start;
    351           }
    352           if (ranges[i].end > range_end && !end_found) {
    353             range_end = ranges[i].end;
    354           }
    355         }
    356       }
    357 
    358       for (codekind in CodeKinds) {
    359         var ticks = CodeKinds[codekind].in_execution;
    360         for (var i = 0; i < ticks.length; i++) {
    361           if (ticks[i].tick < range_start && !start_found) {
    362             range_start = ticks[i].tick;
    363           }
    364           if (ticks[i].tick > range_end && !end_found) {
    365             range_end = ticks[i].tick;
    366           }
    367         }
    368       }
    369     }
    370     // Set pause tolerance to something appropriate for the plot resolution
    371     // to make it easier for gnuplot.
    372     pause_tolerance = (range_end - range_start) / kResX / 10;
    373 
    374     if (typeof result_callback === 'function') {
    375       result_callback(range_start, range_end);
    376     }
    377   };
    378 
    379 
    380   this.assembleOutput = function(output) {
    381     output("set yrange [0:" + (num_timer_event + 1) + "]");
    382     output("set xlabel \"execution time in ms\"");
    383     output("set xrange [" + range_start + ":" + range_end + "]");
    384     output("set style fill pattern 2 bo 1");
    385     output("set style rect fs solid 1 noborder");
    386     output("set style line 1 lt 1 lw 1 lc rgb \"#000000\"");
    387     output("set border 15 lw 0.2");  // Draw thin border box.
    388     output("set style line 2 lt 1 lw 1 lc rgb \"#9944CC\"");
    389     output("set xtics out nomirror");
    390     output("unset key");
    391 
    392     function DrawBarBase(color, start, end, top, bottom) {
    393       obj_index++;
    394       command = "set object " + obj_index + " rect";
    395       command += " from " + start + ", " + top;
    396       command += " to " + end + ", " + bottom;
    397       command += " fc rgb \"" + color + "\"";
    398       output(command);
    399     }
    400 
    401     function DrawBar(row, color, start, end, width) {
    402       DrawBarBase(color, start, end, row + width, row - width);
    403     }
    404 
    405     function DrawHalfBar(row, color, start, end, width) {
    406       DrawBarBase(color, start, end, row, row - width);
    407     }
    408 
    409     var percentages = {};
    410     var total = 0;
    411     for (var name in TimerEvents) {
    412       var event = TimerEvents[name];
    413       var ranges = RestrictRangesTo(event.ranges, range_start, range_end);
    414       ranges = MergeRanges(ranges);
    415       var sum =
    416         ranges.map(function(range) { return range.duration(); })
    417             .reduce(function(a, b) { return a + b; }, 0);
    418       percentages[name] = (sum / (range_end - range_start) * 100).toFixed(1);
    419     }
    420 
    421     // Plot deopts.
    422     deopts.sort(function(a, b) { return b.size - a.size; });
    423     var max_deopt_size = deopts.length > 0 ? deopts[0].size : Infinity;
    424 
    425     for (var i = 0; i < deopts.length; i++) {
    426       var deopt = deopts[i];
    427       DrawHalfBar(kDeoptRow, "#9944CC", deopt.time,
    428                   deopt.time + 10 * pause_tolerance,
    429                   deopt.size / max_deopt_size * kMaxDeoptLength);
    430     }
    431 
    432     // Name Y-axis.
    433     var ytics = [];
    434     for (name in TimerEvents) {
    435       var index = TimerEvents[name].index;
    436       var label = TimerEvents[name].label;
    437       ytics.push('"' + label + ' (' + percentages[name] + '%%)" ' + index);
    438     }
    439     ytics.push('"code kind color coding" ' + kY1Offset);
    440     ytics.push('"code kind in execution" ' + (kY1Offset - 1));
    441     ytics.push('"top ' + kStackFrames + ' js stack frames"' + ' ' +
    442                (kY1Offset - 2));
    443     ytics.push('"pause times" 0');
    444     ytics.push('"max deopt size: ' + (max_deopt_size / 1024).toFixed(1) +
    445                ' kB" ' + kDeoptRow);
    446     output("set ytics out nomirror (" + ytics.join(', ') + ")");
    447 
    448     // Plot timeline.
    449     for (var name in TimerEvents) {
    450       var event = TimerEvents[name];
    451       var ranges = MergeRanges(event.ranges);
    452       for (var i = 0; i < ranges.length; i++) {
    453         DrawBar(event.index, event.color,
    454                 ranges[i].start, ranges[i].end,
    455                 kTimerEventWidth);
    456       }
    457     }
    458 
    459     // Plot code kind gathered from ticks.
    460     for (var name in CodeKinds) {
    461       var code_kind = CodeKinds[name];
    462       var offset = kY1Offset - 1;
    463       // Top most frame.
    464       var row = MergeRanges(TicksToRanges(code_kind.in_execution));
    465       for (var j = 0; j < row.length; j++) {
    466         DrawBar(offset, code_kind.color,
    467                 row[j].start, row[j].end, kExecutionFrameWidth);
    468       }
    469       offset = offset - 2 * kExecutionFrameWidth - kGapWidth;
    470       // Javascript frames.
    471       for (var i = 0; i < kStackFrames; i++) {
    472         offset = offset - 2 * kStackFrameWidth - kGapWidth;
    473         row = MergeRanges(TicksToRanges(code_kind.stack_frames[i]));
    474         for (var j = 0; j < row.length; j++) {
    475           DrawBar(offset, code_kind.color,
    476                   row[j].start, row[j].end, kStackFrameWidth);
    477         }
    478       }
    479     }
    480 
    481     // Add labels as legend for code kind colors.
    482     var padding = kCodeKindLabelPadding * (range_end - range_start) / kResX;
    483     var label_x = range_start;
    484     var label_y = kY1Offset;
    485     for (var name in CodeKinds) {
    486       label_x += padding;
    487       output("set label \"" + name + "\" at " + label_x + "," + label_y +
    488              " textcolor rgb \"" + CodeKinds[name].color + "\"" +
    489              " font \"Helvetica,9'\"");
    490       obj_index++;
    491     }
    492 
    493     if (execution_pauses.length == 0) {
    494       // Force plot and return without plotting execution pause impulses.
    495       output("plot 1/0");
    496       return;
    497     }
    498 
    499     // Label the longest pauses.
    500     execution_pauses =
    501         RestrictRangesTo(execution_pauses, range_start, range_end);
    502     execution_pauses.sort(
    503         function(a, b) { return b.duration() - a.duration(); });
    504 
    505     var max_pause_time = execution_pauses[0].duration();
    506     padding = kPauseLabelPadding * (range_end - range_start) / kResX;
    507     var y_scale = kY1Offset / max_pause_time / 2;
    508     for (var i = 0; i < execution_pauses.length && i < kNumPauseLabels; i++) {
    509       var pause = execution_pauses[i];
    510       var label_content = (pause.duration() | 0) + " ms";
    511       var label_x = pause.end + padding;
    512       var label_y = Math.max(1, (pause.duration() * y_scale));
    513       output("set label \"" + label_content + "\" at " +
    514              label_x + "," + label_y + " font \"Helvetica,7'\"");
    515       obj_index++;
    516     }
    517 
    518     // Scale second Y-axis appropriately.
    519     var y2range = max_pause_time * num_timer_event / kY1Offset * 2;
    520     output("set y2range [0:" + y2range + "]");
    521     // Plot graph with impulses as data set.
    522     output("plot '-' using 1:2 axes x1y2 with impulses ls 1");
    523     for (var i = 0; i < execution_pauses.length; i++) {
    524       var pause = execution_pauses[i];
    525       output(pause.end + " " + pause.duration());
    526       obj_index++;
    527     }
    528     output("e");
    529     return obj_index;
    530   };
    531 }
    532