// Copyright 2011 the V8 project authors. All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following // disclaimer in the documentation and/or other materials provided // with the distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived // from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** * @fileoverview Log Reader is used to process log file produced by V8. */ import { CsvParser } from "./csvparser.mjs"; // Parses dummy variable for readability; export function parseString(field) { return field }; export const parseVarArgs = 'parse-var-args'; // Checks fields for numbers that are not safe integers. Returns true if any are // found. function containsUnsafeInts(fields) { for (let i = 0; i < fields.length; i++) { let field = fields[i]; if ('number' == typeof(field) && !Number.isSafeInteger(field)) return true; } return false; } /** * Base class for processing log files. * * @param {boolean} timedRange Ignore ticks outside timed range. * @param {boolean} pairwiseTimedRange Ignore ticks outside pairs of timer * markers. * @constructor */ export class LogReader { constructor(timedRange=false, pairwiseTimedRange=false, useBigInt=false) { this.dispatchTable_ = new Map(); this.timedRange_ = timedRange; this.pairwiseTimedRange_ = pairwiseTimedRange; if (pairwiseTimedRange) this.timedRange_ = true; this.lineNum_ = 0; this.csvParser_ = new CsvParser(); // Variables for tracking of 'current-time' log entries: this.hasSeenTimerMarker_ = false; this.logLinesSinceLastTimerMarker_ = []; // Flag to parse all numeric fields as BigInt to avoid arithmetic errors // caused by memory addresses being greater than MAX_SAFE_INTEGER this.useBigInt = useBigInt; this.parseFrame = useBigInt ? BigInt : parseInt; this.hasSeenUnsafeIntegers = false; } /** * @param {Object} table A table used for parsing and processing * log records. * exampleDispatchTable = { * "log-entry-XXX": { * parser: [parseString, parseInt, ..., parseVarArgs], * processor: this.processXXX.bind(this) * }, * ... * } */ setDispatchTable(table) { if (Object.getPrototypeOf(table) !== null) { throw new Error("Dispatch expected table.__proto__=null for speedup"); } for (let name in table) { const parser = table[name]; if (parser === undefined) continue; if (!parser.isAsync) parser.isAsync = false; if (!Array.isArray(parser.parsers)) { throw new Error(`Invalid parsers: dispatchTable['${ name}'].parsers should be an Array.`); } let type = typeof parser.processor; if (type !== 'function') { throw new Error(`Invalid processor: typeof dispatchTable['${ name}'].processor is '${type}' instead of 'function'`); } if (!parser.processor.name.startsWith('bound ')) { parser.processor = parser.processor.bind(this); } this.dispatchTable_.set(name, parser); } } /** * A thin wrapper around shell's 'read' function showing a file name on error. */ readFile(fileName) { try { return read(fileName); } catch (e) { printErr(`file="${fileName}": ${e.message || e}`); throw e; } } /** * Used for printing error messages. * * @param {string} str Error message. */ printError(str) { // Do nothing. } /** * Processes a portion of V8 profiler event log. * * @param {string} chunk A portion of log. */ async processLogChunk(chunk) { let end = chunk.length; let current = 0; // Kept for debugging in case of parsing errors. let lineNumber = 0; while (current < end) { const next = chunk.indexOf("\n", current); if (next === -1) break; lineNumber++; const line = chunk.substring(current, next); current = next + 1; await this.processLogLine(line); } } /** * Processes a line of V8 profiler event log. * * @param {string} line A line of log. */ async processLogLine(line) { if (!this.timedRange_) { await this.processLogLine_(line); return; } if (line.startsWith("current-time")) { if (this.hasSeenTimerMarker_) { await this.processLog_(this.logLinesSinceLastTimerMarker_); this.logLinesSinceLastTimerMarker_ = []; // In pairwise mode, a "current-time" line ends the timed range. if (this.pairwiseTimedRange_) { this.hasSeenTimerMarker_ = false; } } else { this.hasSeenTimerMarker_ = true; } } else { if (this.hasSeenTimerMarker_) { this.logLinesSinceLastTimerMarker_.push(line); } else if (!line.startsWith("tick")) { await this.processLogLine_(line); } } } /** * Processes stack record. * * @param {number} pc Program counter. * @param {number} func JS Function. * @param {string[]} stack String representation of a stack. * @return {number[]} Processed stack. */ processStack(pc, func, stack) { const fullStack = func ? [pc, func] : [pc]; let prevFrame = pc; const length = stack.length; for (let i = 0, n = length; i < n; ++i) { const frame = stack[i]; const firstChar = frame[0]; if (firstChar === '+' || firstChar === '-') { // An offset from the previous frame. prevFrame += this.parseFrame(frame); fullStack.push(prevFrame); // Filter out possible 'overflow' string. } else if (firstChar !== 'o') { fullStack.push(this.parseFrame(frame)); } else { console.error(`Dropping unknown tick frame: ${frame}`); } } return fullStack; } /** * Does a dispatch of a log record. * * @param {string[]} fields Log record. * @private */ async dispatchLogRow_(fields) { // Obtain the dispatch. const command = fields[0]; const dispatch = this.dispatchTable_.get(command); if (dispatch === undefined) return; const parsers = dispatch.parsers; const length = parsers.length; // Parse fields. const parsedFields = new Array(length); for (let i = 0; i < length; ++i) { const parser = parsers[i]; if (parser === parseVarArgs) { parsedFields[i] = fields.slice(1 + i); break; } else { parsedFields[i] = parser(fields[1 + i]); } } if (!this.useBigInt) { if (!this.hasSeenUnsafeIntegers && containsUnsafeInts(parsedFields)) { console.warn(`Log line containts unsafe integers: ${fields}`); this.hasSeenUnsafeIntegers = true; } } // Run the processor. await dispatch.processor(...parsedFields); } /** * Processes log lines. * * @param {string[]} lines Log lines. * @private */ async processLog_(lines) { for (let i = 0, n = lines.length; i < n; ++i) { await this.processLogLine_(lines[i]); } } /** * Processes a single log line. * * @param {String} a log line * @private */ async processLogLine_(line) { if (line.length > 0) { try { const fields = this.csvParser_.parseLine(line); await this.dispatchLogRow_(fields); } catch (e) { this.printError(`line ${this.lineNum_ + 1}: ${e.message || e}\n${e.stack}`); } } this.lineNum_++; } }