1// Copyright 2017 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// ===========================================================================
6class MapProcessor extends LogReader {
7  constructor() {
8    super();
9    this.dispatchTable_ = {
10      'code-creation': {
11        parsers: [parseString, parseInt, parseInt, parseInt, parseInt,
12          parseString, parseVarArgs],
13        processor: this.processCodeCreation
14      },
15      'code-move': {
16        parsers: [parseInt, parseInt],
17        'sfi-move': {
18          parsers: [parseInt, parseInt],
19          processor: this.processCodeMove
20        },
21        'code-delete': {
22          parsers: [parseInt],
23          processor: this.processCodeDelete
24        },
25        processor: this.processFunctionMove
26      },
27      'map-create': {
28        parsers: [parseInt, parseInt, parseString],
29        processor: this.processMapCreate
30      },
31      'map': {
32        parsers: [parseString, parseInt, parseInt, parseInt, parseInt, parseInt,
33          parseString, parseString, parseString
34        ],
35        processor: this.processMap
36      },
37      'map-details': {
38        parsers: [parseInt, parseInt, parseString],
39        processor: this.processMapDetails
40      }
41    };
42    this.profile_ = new Profile();
43    this.timeline_ = new Timeline();
44  }
45
46  printError(str) {
47    console.error(str);
48    throw str
49  }
50
51  processString(string) {
52    let end = string.length;
53    let current = 0;
54    let next = 0;
55    let line;
56    let i = 0;
57    let entry;
58    try {
59      while (current < end) {
60        next = string.indexOf("\n", current);
61        if (next === -1) break;
62        i++;
63        line = string.substring(current, next);
64        current = next + 1;
65        this.processLogLine(line);
66      }
67    } catch(e) {
68      console.error("Error occurred during parsing, trying to continue: " + e);
69    }
70    return this.finalize();
71  }
72
73  processLogFile(fileName) {
74    this.collectEntries = true
75    this.lastLogFileName_ = fileName;
76    let line;
77    while (line = readline()) {
78      this.processLogLine(line);
79    }
80    return this.finalize();
81  }
82
83  finalize() {
84    // TODO(cbruni): print stats;
85    this.timeline_.finalize();
86    return this.timeline_;
87  }
88
89  addEntry(entry) {
90    this.entries.push(entry);
91  }
92
93  /**
94   * Parser for dynamic code optimization state.
95   */
96  parseState(s) {
97    switch (s) {
98      case "":
99        return Profile.CodeState.COMPILED;
100      case "~":
101        return Profile.CodeState.OPTIMIZABLE;
102      case "*":
103        return Profile.CodeState.OPTIMIZED;
104    }
105    throw new Error("unknown code state: " + s);
106  }
107
108  processCodeCreation(
109    type, kind, timestamp, start, size, name, maybe_func) {
110    if (maybe_func.length) {
111      let funcAddr = parseInt(maybe_func[0]);
112      let state = this.parseState(maybe_func[1]);
113      this.profile_.addFuncCode(type, name, timestamp, start, size, funcAddr, state);
114    } else {
115      this.profile_.addCode(type, name, timestamp, start, size);
116    }
117  }
118
119  processCodeMove(from, to) {
120    this.profile_.moveCode(from, to);
121  }
122
123  processCodeDelete(start) {
124    this.profile_.deleteCode(start);
125  }
126
127  processFunctionMove(from, to) {
128    this.profile_.moveFunc(from, to);
129  }
130
131  formatPC(pc, line, column) {
132    let entry = this.profile_.findEntry(pc);
133    if (!entry) return "<unknown>"
134    if (entry.type == "Builtin") {
135      return entry.name;
136    }
137    let name = entry.func.getName();
138    let re = /(.*):[0-9]+:[0-9]+$/;
139    let array = re.exec(name);
140    if (!array) {
141      entry = name;
142    } else {
143      entry = entry.getState() + array[1];
144    }
145    return entry + ":" + line + ":" + column;
146  }
147
148  processMap(type, time, from, to, pc, line, column, reason, name) {
149    time = parseInt(time);
150    if (type == "Deprecate") return this.deprecateMap(type, time, from);
151    from = this.getExistingMap(from, time);
152    to = this.getExistingMap(to, time);
153    let edge = new Edge(type, name, reason, time, from, to);
154    edge.filePosition = this.formatPC(pc, line, column);
155    edge.finishSetup();
156  }
157
158  deprecateMap(type, time, id) {
159    this.getExistingMap(id, time).deprecate();
160  }
161
162  processMapCreate(time, id, string) {
163    // map-create events might override existing maps if the addresses get
164    // rcycled. Hence we do not check for existing maps.
165    let map = this.createMap(id, time);
166    map.description = string;
167  }
168
169  processMapDetails(time, id, string) {
170    //TODO(cbruni): fix initial map logging.
171    let map = this.getExistingMap(id, time);
172    if (!map.description) {
173      map.description = string;
174    }
175  }
176
177  createMap(id, time) {
178    let map = new V8Map(id, time);
179    this.timeline_.push(map);
180    return map;
181  }
182
183  getExistingMap(id, time) {
184    if (id === 0) return undefined;
185    let map = V8Map.get(id);
186    if (map === undefined) {
187      console.error("No map details provided: id=" + id);
188      // Manually patch in a map to continue running.
189      return this.createMap(id, time);
190    };
191    return map;
192  }
193}
194
195// ===========================================================================
196
197class V8Map {
198  constructor(id, time = -1) {
199    if (!id) throw "Invalid ID";
200    this.id = id;
201    this.time = time;
202    if (!(time > 0)) throw "Invalid time";
203    this.description = "";
204    this.edge = void 0;
205    this.children = [];
206    this.depth = 0;
207    this._isDeprecated = false;
208    this.deprecationTargets = null;
209    V8Map.set(id, this);
210    this.leftId = 0;
211    this.rightId = 0;
212  }
213
214  finalize(id) {
215    // Initialize preorder tree traversal Ids for fast subtree inclusion checks
216    if (id <= 0) throw "invalid id";
217    let currentId = id;
218    this.leftId = currentId
219    this.children.forEach(edge => {
220      let map = edge.to;
221      currentId = map.finalize(currentId + 1);
222    });
223    this.rightId = currentId + 1;
224    return currentId + 1;
225  }
226
227  parent() {
228    if (this.edge === void 0) return void 0;
229    return this.edge.from;
230  }
231
232  isDeprecated() {
233    return this._isDeprecated;
234  }
235
236  deprecate() {
237    this._isDeprecated = true;
238  }
239
240  isRoot() {
241    return this.edge == void 0 || this.edge.from == void 0;
242  }
243
244  contains(map) {
245    return this.leftId < map.leftId && map.rightId < this.rightId;
246  }
247
248  addEdge(edge) {
249    this.children.push(edge);
250  }
251
252  chunkIndex(chunks) {
253    // Did anybody say O(n)?
254    for (let i = 0; i < chunks.length; i++) {
255      let chunk = chunks[i];
256      if (chunk.isEmpty()) continue;
257      if (chunk.last().time < this.time) continue;
258      return i;
259    }
260    return -1;
261  }
262
263  position(chunks) {
264    let index = this.chunkIndex(chunks);
265    let xFrom = (index + 0.5) * kChunkWidth;
266    let yFrom = kChunkHeight - chunks[index].yOffset(this);
267    return [xFrom, yFrom];
268  }
269
270  transitions() {
271    let transitions = Object.create(null);
272    let current = this;
273    while (current) {
274      let edge = current.edge;
275      if (edge && edge.isTransition()) {
276        transitions[edge.name] = edge;
277      }
278      current = current.parent()
279    }
280    return transitions;
281  }
282
283  getType() {
284    return this.edge === void 0 ? "new" : this.edge.type;
285  }
286
287  getParents() {
288    let parents = [];
289    let current = this.parent();
290    while (current) {
291      parents.push(current);
292      current = current.parent();
293    }
294    return parents;
295  }
296
297  static get(id) {
298    if (!this.cache) return undefined;
299    return this.cache.get(id);
300  }
301
302  static set(id, map) {
303    if (!this.cache) this.cache = new Map();
304    this.cache.set(id, map);
305  }
306}
307
308
309// ===========================================================================
310class Edge {
311  constructor(type, name, reason, time, from, to) {
312    this.type = type;
313    this.name = name;
314    this.reason = reason;
315    this.time = time;
316    this.from = from;
317    this.to = to;
318    this.filePosition = "";
319  }
320
321  finishSetup() {
322    if (this.from) this.from.addEdge(this);
323    if (this.to) {
324      this.to.edge = this;
325      if (this.to === this.from) throw "From and to must be distinct.";
326      if (this.from) {
327        if (this.to.time < this.from.time) {
328          console.error("invalid time order");
329        }
330        let newDepth = this.from.depth + 1;
331        if (this.to.depth > 0 && this.to.depth != newDepth) {
332          console.error("Depth has already been initialized");
333        }
334        this.to.depth = newDepth;
335      }
336    }
337  }
338
339  chunkIndex(chunks) {
340    // Did anybody say O(n)?
341    for (let i = 0; i < chunks.length; i++) {
342      let chunk = chunks[i];
343      if (chunk.isEmpty()) continue;
344      if (chunk.last().time < this.time) continue;
345      return i;
346    }
347    return -1;
348  }
349
350  parentEdge() {
351    if (!this.from) return undefined;
352    return this.from.edge;
353  }
354
355  chainLength() {
356    let length = 0;
357    let prev = this;
358    while (prev) {
359      prev = this.parent;
360      length++;
361    }
362    return length;
363  }
364
365  isTransition() {
366    return this.type == "Transition"
367  }
368
369  isFastToSlow() {
370    return this.type == "Normalize"
371  }
372
373  isSlowToFast() {
374    return this.type == "SlowToFast"
375  }
376
377  isInitial() {
378    return this.type == "InitialMap"
379  }
380
381  isReplaceDescriptors() {
382    return this.type == "ReplaceDescriptors"
383  }
384
385  isCopyAsPrototype() {
386    return this.reason == "CopyAsPrototype"
387  }
388
389  isOptimizeAsPrototype() {
390    return this.reason == "OptimizeAsPrototype"
391  }
392
393  symbol() {
394    if (this.isTransition()) return "+";
395    if (this.isFastToSlow()) return "⊡";
396    if (this.isSlowToFast()) return "⊛";
397    if (this.isReplaceDescriptors()) {
398      if (this.name) return "+";
399      return "∥";
400    }
401    return "";
402  }
403
404  toString() {
405    let s = this.symbol();
406    if (this.isTransition()) return s + this.name;
407    if (this.isFastToSlow()) return s + this.reason;
408    if (this.isCopyAsPrototype()) return s + "Copy as Prototype";
409    if (this.isOptimizeAsPrototype()) {
410      return s + "Optimize as Prototype";
411    }
412    if (this.isReplaceDescriptors() && this.name) {
413      return this.type + " " + this.symbol() + this.name;
414    }
415    return this.type + " " + (this.reason ? this.reason : "") + " " +
416      (this.name ? this.name : "")
417  }
418}
419
420
421// ===========================================================================
422class Marker {
423  constructor(time, name) {
424    this.time = parseInt(time);
425    this.name = name;
426  }
427}
428
429// ===========================================================================
430class Timeline {
431  constructor() {
432    this.values = [];
433    this.transitions = new Map();
434    this.markers = [];
435    this.startTime = 0;
436    this.endTime = 0;
437  }
438
439  push(map) {
440    let time = map.time;
441    if (!this.isEmpty() && this.last().time > time) {
442      // Invalid insertion order, might happen without --single-process,
443      // finding insertion point.
444      let insertionPoint = this.find(time);
445      this.values.splice(insertionPoint, map);
446    } else {
447      this.values.push(map);
448    }
449    if (time > 0) {
450      this.endTime = Math.max(this.endTime, time);
451      if (this.startTime === 0) {
452        this.startTime = time;
453      } else {
454        this.startTime = Math.min(this.startTime, time);
455      }
456    }
457  }
458
459  addMarker(time, message) {
460    this.markers.push(new Marker(time, message));
461  }
462
463  finalize() {
464    let id = 0;
465    this.forEach(map => {
466      if (map.isRoot()) id = map.finalize(id + 1);
467      if (map.edge && map.edge.name) {
468        let edge = map.edge;
469        let list = this.transitions.get(edge.name);
470        if (list === undefined) {
471          this.transitions.set(edge.name, [edge]);
472        } else {
473          list.push(edge);
474        }
475      }
476    });
477    this.markers.sort((a, b) => b.time - a.time);
478  }
479
480  at(index) {
481    return this.values[index]
482  }
483
484  isEmpty() {
485    return this.size() == 0
486  }
487
488  size() {
489    return this.values.length
490  }
491
492  first() {
493    return this.values.first()
494  }
495
496  last() {
497    return this.values.last()
498  }
499
500  duration() {
501    return this.last().time - this.first().time
502  }
503
504  forEachChunkSize(count, fn) {
505    const increment = this.duration() / count;
506    let currentTime = this.first().time + increment;
507    let index = 0;
508    for (let i = 0; i < count; i++) {
509      let nextIndex = this.find(currentTime, index);
510      let nextTime = currentTime + increment;
511      fn(index, nextIndex, currentTime, nextTime);
512      index = nextIndex
513      currentTime = nextTime;
514    }
515  }
516
517  chunkSizes(count) {
518    let chunks = [];
519    this.forEachChunkSize(count, (start, end) => chunks.push(end - start));
520    return chunks;
521  }
522
523  chunks(count) {
524    let chunks = [];
525    let emptyMarkers = [];
526    this.forEachChunkSize(count, (start, end, startTime, endTime) => {
527      let items = this.values.slice(start, end);
528      let markers = this.markersAt(startTime, endTime);
529      chunks.push(new Chunk(chunks.length, startTime, endTime, items, markers));
530    });
531    return chunks;
532  }
533
534  range(start, end) {
535    const first = this.find(start);
536    if (first < 0) return [];
537    const last = this.find(end, first);
538    return this.values.slice(first, last);
539  }
540
541  find(time, offset = 0) {
542    return this.basicFind(this.values, each => each.time - time, offset);
543  }
544
545  markersAt(startTime, endTime) {
546    let start = this.basicFind(this.markers, each => each.time - startTime);
547    let end = this.basicFind(this.markers, each => each.time - endTime, start);
548    return this.markers.slice(start, end);
549  }
550
551  basicFind(array, cmp, offset = 0) {
552    let min = offset;
553    let max = array.length;
554    while (min < max) {
555      let mid = min + Math.floor((max - min) / 2);
556      let result = cmp(array[mid]);
557      if (result > 0) {
558        max = mid - 1;
559      } else {
560        min = mid + 1;
561      }
562    }
563    return min;
564  }
565
566  count(filter) {
567    return this.values.reduce((sum, each) => {
568      return sum + (filter(each) ? 1 : 0);
569    }, 0);
570  }
571
572  filter(predicate) {
573    return this.values.filter(predicate);
574  }
575
576  filterUniqueTransitions(filter) {
577    // Returns a list of Maps whose parent is not in the list.
578    return this.values.filter(map => {
579      if (!filter(map)) return false;
580      let parent = map.parent();
581      if (!parent) return true;
582      return !filter(parent);
583    });
584  }
585
586  depthHistogram() {
587    return this.values.histogram(each => each.depth);
588  }
589
590  fanOutHistogram() {
591    return this.values.histogram(each => each.children.length);
592  }
593
594  forEach(fn) {
595    return this.values.forEach(fn)
596  }
597}
598
599
600// ===========================================================================
601class Chunk {
602  constructor(index, start, end, items, markers) {
603    this.index = index;
604    this.start = start;
605    this.end = end;
606    this.items = items;
607    this.markers = markers
608    this.height = 0;
609  }
610
611  isEmpty() {
612    return this.items.length == 0;
613  }
614
615  last() {
616    return this.at(this.size() - 1);
617  }
618
619  first() {
620    return this.at(0);
621  }
622
623  at(index) {
624    return this.items[index];
625  }
626
627  size() {
628    return this.items.length;
629  }
630
631  yOffset(map) {
632    // items[0]   == oldest map, displayed at the top of the chunk
633    // items[n-1] == youngest map, displayed at the bottom of the chunk
634    return (1 - (this.indexOf(map) + 0.5) / this.size()) * this.height;
635  }
636
637  indexOf(map) {
638    return this.items.indexOf(map);
639  }
640
641  has(map) {
642    if (this.isEmpty()) return false;
643    return this.first().time <= map.time && map.time <= this.last().time;
644  }
645
646  next(chunks) {
647    return this.findChunk(chunks, 1);
648  }
649
650  prev(chunks) {
651    return this.findChunk(chunks, -1);
652  }
653
654  findChunk(chunks, delta) {
655    let i = this.index + delta;
656    let chunk = chunks[i];
657    while (chunk && chunk.size() == 0) {
658      i += delta;
659      chunk = chunks[i]
660    }
661    return chunk;
662  }
663
664  getTransitionBreakdown() {
665    return BreakDown(this.items, map => map.getType())
666  }
667
668  getUniqueTransitions() {
669    // Filter out all the maps that have parents within the same chunk.
670    return this.items.filter(map => !map.parent() || !this.has(map.parent()));
671  }
672}
673
674
675// ===========================================================================
676function BreakDown(list, map_fn) {
677  if (map_fn === void 0) {
678    map_fn = each => each;
679  }
680  let breakdown = {__proto__:null};
681  list.forEach(each=> {
682    let type = map_fn(each);
683    let v = breakdown[type];
684    breakdown[type] = (v | 0) + 1
685  });
686  return Object.entries(breakdown)
687    .sort((a,b) => a[1] - b[1]);
688}
689
690
691// ===========================================================================
692class ArgumentsProcessor extends BaseArgumentsProcessor {
693  getArgsDispatch() {
694    return {
695      '--range': ['range', 'auto,auto',
696        'Specify the range limit as [start],[end]'
697      ],
698      '--source-map': ['sourceMap', null,
699        'Specify the source map that should be used for output'
700      ]
701    };
702  }
703
704  getDefaultResults() {
705    return {
706      logFileName: 'v8.log',
707      range: 'auto,auto',
708    };
709  }
710}
711