| // Copyright 2020 the V8 project authors. All rights reserved. | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | import {LogReader, parseString, parseVarArgs} from '../logreader.mjs'; | 
 | import {Profile} from '../profile.mjs'; | 
 |  | 
 | import {DeoptLogEntry} from './log/deopt.mjs'; | 
 | import {IcLogEntry} from './log/ic.mjs'; | 
 | import {Edge, MapLogEntry} from './log/map.mjs'; | 
 | import {Timeline} from './timeline.mjs'; | 
 |  | 
 | // =========================================================================== | 
 |  | 
 | export class Processor extends LogReader { | 
 |   _profile = new Profile(); | 
 |   _mapTimeline = new Timeline(); | 
 |   _icTimeline = new Timeline(); | 
 |   _deoptTimeline = new Timeline(); | 
 |   _formatPCRegexp = /(.*):[0-9]+:[0-9]+$/; | 
 |   MAJOR_VERSION = 7; | 
 |   MINOR_VERSION = 6; | 
 |   constructor(logString) { | 
 |     super(); | 
 |     this.propertyICParser = [ | 
 |       parseInt, parseInt, parseInt, parseInt, parseString, parseString, | 
 |       parseString, parseString, parseString, parseString | 
 |     ]; | 
 |     this.dispatchTable_ = { | 
 |       __proto__: null, | 
 |       'code-creation': { | 
 |         parsers: [ | 
 |           parseString, parseInt, parseInt, parseInt, parseInt, parseString, | 
 |           parseVarArgs | 
 |         ], | 
 |         processor: this.processCodeCreation | 
 |       }, | 
 |       'code-deopt': { | 
 |         parsers: [ | 
 |           parseInt, parseInt, parseInt, parseInt, parseInt, parseString, | 
 |           parseString, parseString | 
 |         ], | 
 |         processor: this.processCodeDeopt | 
 |       }, | 
 |       'v8-version': { | 
 |         parsers: [ | 
 |           parseInt, | 
 |           parseInt, | 
 |         ], | 
 |         processor: this.processV8Version | 
 |       }, | 
 |       'script-source': { | 
 |         parsers: [parseInt, parseString, parseString], | 
 |         processor: this.processScriptSource | 
 |       }, | 
 |       'code-move': | 
 |           {parsers: [parseInt, parseInt], processor: this.processCodeMove}, | 
 |       'code-delete': {parsers: [parseInt], processor: this.processCodeDelete}, | 
 |       'sfi-move': | 
 |           {parsers: [parseInt, parseInt], processor: this.processFunctionMove}, | 
 |       'map-create': | 
 |           {parsers: [parseInt, parseString], processor: this.processMapCreate}, | 
 |       'map': { | 
 |         parsers: [ | 
 |           parseString, parseInt, parseString, parseString, parseInt, parseInt, | 
 |           parseInt, parseString, parseString | 
 |         ], | 
 |         processor: this.processMap | 
 |       }, | 
 |       'map-details': { | 
 |         parsers: [parseInt, parseString, parseString], | 
 |         processor: this.processMapDetails | 
 |       }, | 
 |       'LoadGlobalIC': { | 
 |         parsers: this.propertyICParser, | 
 |         processor: this.processPropertyIC.bind(this, 'LoadGlobalIC') | 
 |       }, | 
 |       'StoreGlobalIC': { | 
 |         parsers: this.propertyICParser, | 
 |         processor: this.processPropertyIC.bind(this, 'StoreGlobalIC') | 
 |       }, | 
 |       'LoadIC': { | 
 |         parsers: this.propertyICParser, | 
 |         processor: this.processPropertyIC.bind(this, 'LoadIC') | 
 |       }, | 
 |       'StoreIC': { | 
 |         parsers: this.propertyICParser, | 
 |         processor: this.processPropertyIC.bind(this, 'StoreIC') | 
 |       }, | 
 |       'KeyedLoadIC': { | 
 |         parsers: this.propertyICParser, | 
 |         processor: this.processPropertyIC.bind(this, 'KeyedLoadIC') | 
 |       }, | 
 |       'KeyedStoreIC': { | 
 |         parsers: this.propertyICParser, | 
 |         processor: this.processPropertyIC.bind(this, 'KeyedStoreIC') | 
 |       }, | 
 |       'StoreInArrayLiteralIC': { | 
 |         parsers: this.propertyICParser, | 
 |         processor: this.processPropertyIC.bind(this, 'StoreInArrayLiteralIC') | 
 |       }, | 
 |     }; | 
 |     if (logString) this.processString(logString); | 
 |   } | 
 |  | 
 |   printError(str) { | 
 |     console.error(str); | 
 |     throw str | 
 |   } | 
 |  | 
 |   processString(string) { | 
 |     let end = string.length; | 
 |     let current = 0; | 
 |     let next = 0; | 
 |     let line; | 
 |     let i = 0; | 
 |     let entry; | 
 |     try { | 
 |       while (current < end) { | 
 |         next = string.indexOf('\n', current); | 
 |         if (next === -1) break; | 
 |         i++; | 
 |         line = string.substring(current, next); | 
 |         current = next + 1; | 
 |         this.processLogLine(line); | 
 |       } | 
 |     } catch (e) { | 
 |       console.error(`Error occurred during parsing, trying to continue: ${e}`); | 
 |     } | 
 |     this.finalize(); | 
 |   } | 
 |  | 
 |   processLogFile(fileName) { | 
 |     this.collectEntries = true; | 
 |     this.lastLogFileName_ = fileName; | 
 |     let i = 1; | 
 |     let line; | 
 |     try { | 
 |       while (line = readline()) { | 
 |         this.processLogLine(line); | 
 |         i++; | 
 |       } | 
 |     } catch (e) { | 
 |       console.error( | 
 |           `Error occurred during parsing line ${i}` + | 
 |           ', trying to continue: ' + e); | 
 |     } | 
 |     this.finalize(); | 
 |   } | 
 |  | 
 |   finalize() { | 
 |     // TODO(cbruni): print stats; | 
 |     this._mapTimeline.transitions = new Map(); | 
 |     let id = 0; | 
 |     this._mapTimeline.forEach(map => { | 
 |       if (map.isRoot()) id = map.finalizeRootMap(id + 1); | 
 |       if (map.edge && map.edge.name) { | 
 |         const edge = map.edge; | 
 |         const list = this._mapTimeline.transitions.get(edge.name); | 
 |         if (list === undefined) { | 
 |           this._mapTimeline.transitions.set(edge.name, [edge]); | 
 |         } else { | 
 |           list.push(edge); | 
 |         } | 
 |       } | 
 |     }); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Parser for dynamic code optimization state. | 
 |    */ | 
 |   parseState(s) { | 
 |     switch (s) { | 
 |       case '': | 
 |         return Profile.CodeState.COMPILED; | 
 |       case '~': | 
 |         return Profile.CodeState.OPTIMIZABLE; | 
 |       case '*': | 
 |         return Profile.CodeState.OPTIMIZED; | 
 |     } | 
 |     throw new Error(`unknown code state: ${s}`); | 
 |   } | 
 |  | 
 |   processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) { | 
 |     if (maybe_func.length) { | 
 |       const funcAddr = parseInt(maybe_func[0]); | 
 |       const state = this.parseState(maybe_func[1]); | 
 |       this._profile.addFuncCode( | 
 |           type, name, timestamp, start, size, funcAddr, state); | 
 |     } else { | 
 |       this._profile.addCode(type, name, timestamp, start, size); | 
 |     } | 
 |   } | 
 |  | 
 |   processCodeDeopt( | 
 |       timestamp, codeSize, instructionStart, inliningId, scriptOffset, | 
 |       deoptKind, deoptLocation, deoptReason) { | 
 |     this._deoptTimeline.push(new DeoptLogEntry(deoptKind, timestamp)); | 
 |   } | 
 |  | 
 |   processV8Version(majorVersion, minorVersion) { | 
 |     if ((majorVersion == this.MAJOR_VERSION && | 
 |          minorVersion <= this.MINOR_VERSION) || | 
 |         (majorVersion < this.MAJOR_VERSION)) { | 
 |       window.alert( | 
 |           `Unsupported version ${majorVersion}.${minorVersion}. \n` + | 
 |           `Please use the matching tool for given the V8 version.`); | 
 |     } | 
 |   } | 
 |  | 
 |   processScriptSource(scriptId, url, source) { | 
 |     this._profile.addScriptSource(scriptId, url, source); | 
 |   } | 
 |  | 
 |   processCodeMove(from, to) { | 
 |     this._profile.moveCode(from, to); | 
 |   } | 
 |  | 
 |   processCodeDelete(start) { | 
 |     this._profile.deleteCode(start); | 
 |   } | 
 |  | 
 |   processFunctionMove(from, to) { | 
 |     this._profile.moveFunc(from, to); | 
 |   } | 
 |  | 
 |   formatName(entry) { | 
 |     if (!entry) return '<unknown>'; | 
 |     let name = entry.func.getName(); | 
 |     let re = /(.*):[0-9]+:[0-9]+$/; | 
 |     let array = re.exec(name); | 
 |     if (!array) return name; | 
 |     return entry.getState() + array[1]; | 
 |   } | 
 |  | 
 |   processPropertyIC( | 
 |       type, pc, time, line, column, old_state, new_state, map, key, modifier, | 
 |       slow_reason) { | 
 |     let fnName = this.functionName(pc); | 
 |     let parts = fnName.split(' '); | 
 |     let fileName = parts[parts.length - 1]; | 
 |     let script = this.getScript(fileName); | 
 |     // TODO: Use SourcePosition here directly | 
 |     let entry = new IcLogEntry( | 
 |         type, fnName, time, line, column, key, old_state, new_state, map, | 
 |         slow_reason, script, modifier); | 
 |     if (script) { | 
 |       entry.sourcePosition = script.addSourcePosition(line, column, entry); | 
 |     } | 
 |     this._icTimeline.push(entry); | 
 |   } | 
 |  | 
 |   functionName(pc) { | 
 |     let entry = this._profile.findEntry(pc); | 
 |     return this.formatName(entry); | 
 |   } | 
 |   formatPC(pc, line, column) { | 
 |     let entry = this._profile.findEntry(pc); | 
 |     if (!entry) return '<unknown>' | 
 |       if (entry.type === 'Builtin') { | 
 |         return entry.name; | 
 |       } | 
 |     let name = entry.func.getName(); | 
 |     let array = this._formatPCRegexp.exec(name); | 
 |     if (array === null) { | 
 |       entry = name; | 
 |     } else { | 
 |       entry = entry.getState() + array[1]; | 
 |     } | 
 |     return entry + ':' + line + ':' + column; | 
 |   } | 
 |  | 
 |   processFileName(filePositionLine) { | 
 |     if (!filePositionLine.includes(' ')) return; | 
 |     // Try to handle urls with file positions: https://foo.bar.com/:17:330" | 
 |     filePositionLine = filePositionLine.split(' '); | 
 |     let parts = filePositionLine[1].split(':'); | 
 |     if (parts[0].length <= 5) return parts[0] + ':' + parts[1]; | 
 |     return parts[1]; | 
 |   } | 
 |  | 
 |   processMap(type, time, from, to, pc, line, column, reason, name) { | 
 |     let time_ = parseInt(time); | 
 |     if (type === 'Deprecate') return this.deprecateMap(type, time_, from); | 
 |     let from_ = this.getExistingMapEntry(from, time_); | 
 |     let to_ = this.getExistingMapEntry(to, time_); | 
 |     // TODO: use SourcePosition directly. | 
 |     let edge = new Edge(type, name, reason, time, from_, to_); | 
 |     to_.filePosition = this.formatPC(pc, line, column); | 
 |     let fileName = this.processFileName(to_.filePosition); | 
 |     // TODO: avoid undefined source positions. | 
 |     if (fileName !== undefined) { | 
 |       to_.script = this.getScript(fileName); | 
 |     } | 
 |     if (to_.script) { | 
 |       to_.sourcePosition = to_.script.addSourcePosition(line, column, to_) | 
 |     } | 
 |     edge.finishSetup(); | 
 |   } | 
 |  | 
 |   deprecateMap(type, time, id) { | 
 |     this.getExistingMapEntry(id, time).deprecate(); | 
 |   } | 
 |  | 
 |   processMapCreate(time, id) { | 
 |     // map-create events might override existing maps if the addresses get | 
 |     // recycled. Hence we do not check for existing maps. | 
 |     let map = this.createMapEntry(id, time); | 
 |   } | 
 |  | 
 |   processMapDetails(time, id, string) { | 
 |     // TODO(cbruni): fix initial map logging. | 
 |     let map = this.getExistingMapEntry(id, time); | 
 |     map.description = string; | 
 |   } | 
 |  | 
 |   createMapEntry(id, time) { | 
 |     let map = new MapLogEntry(id, time); | 
 |     this._mapTimeline.push(map); | 
 |     return map; | 
 |   } | 
 |  | 
 |   getExistingMapEntry(id, time) { | 
 |     if (id === '0x000000000000') return undefined; | 
 |     let map = MapLogEntry.get(id, time); | 
 |     if (map === undefined) { | 
 |       console.error(`No map details provided: id=${id}`); | 
 |       // Manually patch in a map to continue running. | 
 |       return this.createMapEntry(id, time); | 
 |     }; | 
 |     return map; | 
 |   } | 
 |  | 
 |   getScript(url) { | 
 |     const script = this._profile.getScript(url); | 
 |     // TODO create placeholder script for empty urls. | 
 |     if (script === undefined) { | 
 |       console.error(`Could not find script for url: '${url}'`) | 
 |     } | 
 |     return script; | 
 |   } | 
 |  | 
 |   get icTimeline() { | 
 |     return this._icTimeline; | 
 |   } | 
 |  | 
 |   get mapTimeline() { | 
 |     return this._mapTimeline; | 
 |   } | 
 |  | 
 |   get deoptTimeline() { | 
 |     return this._deoptTimeline; | 
 |   } | 
 |  | 
 |   get scripts() { | 
 |     return this._profile.scripts_.filter(script => script !== undefined); | 
 |   } | 
 | } |