current commit
261004.7 · deps: update V8 to 15.2.124.38
// Copyright 2026 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.

const printErr = globalThis.printErr;
globalThis.console = {
  warn: (...args) => printErr('console.warn:', ...args),
  log: (...args) => printErr('console.log:', ...args),
  error: (...args) => printErr('console.error:', ...args)
};

import {Processor} from './processor.mjs';
import {Profile} from '../profile.mjs';

class EventFilter {
  constructor() {
    this.script = null;
    this.scriptId = null;
    this.functionName = null;
    this.file = null;
    this.lineRange = null;
    this.timeRange = null;
    this.line = undefined;
    this.column = undefined;
    this.map = null;
  }

  static get help() {
    return `Filter Options:
  --script=<name>      Filter by script URL or name
  --script-id=<id>     Filter by script ID
  --file=<path>[:line[:col]] Filter by file path and optional line/column
  --location=<path>[:line[:col]] Alias for --file
  --function=<name>    Filter by function name
  --time=<range>       Filter by time range (e.g., 1000..5000, 1000:5000)
  --map=<id>          Filter by map ID`;
  }

  parse(args) {
    for (const arg of args) {
      if (this.parseScript(arg)) continue;
      if (this.parseScriptId(arg)) continue;
      if (this.parseFile(arg)) continue;
      if (this.parseFunctionName(arg)) continue;
      if (this.parseTimeRange(arg)) continue;
      if (this.parseMap(arg)) continue;
    }
  }

  hasFilters() {
    return this.script !== null || this.scriptId !== null ||
        this.functionName !== null || this.file !== null ||
        this.lineRange !== null || this.timeRange !== null ||
        this.line !== undefined || this.column !== undefined ||
        this.map !== null;
  }

  parseScript(arg) {
    if (!arg.startsWith('--script=')) return false;
    const val = arg.substring('--script='.length);

    if (/^\d+$/.test(val)) {
      this._parseScriptId(val);
    } else {
      if (this.scriptId !== null || this.file) {
        throw new Error('Cannot use both --script and --script-id or --file');
      }
      this.script = val;
    }
    return true;
  }

  parseScriptId(arg) {
    if (!arg.startsWith('--script-id=')) return false;
    this._parseScriptId(arg.substring('--script-id='.length));
    return true;
  }

  _parseScriptId(val) {
    if (this.script || this.file) {
      throw new Error('Cannot use both --script-id and --script or --file');
    }
    this.scriptId = parseInt(val);
  }

  parseFile(arg) {
    const isFile = arg.startsWith('--file=');
    const isLocation = arg.startsWith('--location=');
    if (!isFile && !isLocation) return false;

    if (this.script || this.scriptId !== null) {
      throw new Error(
          'Cannot use both --file/--location and --script or --script-id');
    }
    const prefix = isFile ? '--file=' : '--location=';
    const filter = arg.substring(prefix.length);

    // Check for path:line:column
    let match = filter.match(/(.+):(\d+):(\d+)$/);
    if (match) {
      this.file = match[1];
      this.line = parseInt(match[2]);
      this.column = parseInt(match[3]);
      return true;
    }

    // Check for path:range
    match = filter.match(/(.+):(\d+[:.\s-]{1,3}\d+)$/);
    if (match) {
      this.file = match[1];
      this.lineRange = this.parseRange(match[2], 'line');
      return true;
    }

    // Check for path:line
    match = filter.match(/(.+):(\d+)$/);
    if (match) {
      this.file = match[1];
      this.line = parseInt(match[2]);
      return true;
    }

    // Fallback to just path
    this.file = filter;
    return true;
  }

  parseMap(arg) {
    if (!arg.startsWith('--map=')) return false;
    this.map = arg.substring('--map='.length);
    return true;
  }

  parseFunctionName(arg) {
    if (!arg.startsWith('--function=')) return false;
    this.functionName = arg.substring('--function='.length);
    return true;
  }

  parseTimeRange(arg) {
    if (!arg.startsWith('--time=')) return false;
    const timeVal = arg.substring('--time='.length);
    this.timeRange = this.parseRange(timeVal, 'time');
    return true;
  }

  parseRange(str, name) {
    const match = str.match(/^(\d+)[:.\s-]{1,3}(\d+)$/);
    if (!match) {
      throw new Error(`Invalid ${name} range: ${
          str}. Format: start..end, start:end, or "start end"`);
    }

    let start = parseInt(match[1]);
    let end = parseInt(match[2]);

    if (start > end) {
      [start, end] = [end, start];
    }

    return {start, end};
  }

  filterCode(entry, processor) {
    const profileEntry = entry.entry;
    const script =
        profileEntry ? processor.getProfileEntryScript(profileEntry) : null;
    return this._matchesCommon(entry, script);
  }

  filterCodeBasic(entry, processor) {
    const profileEntry = entry.entry;
    const script =
        profileEntry ? processor.getProfileEntryScript(profileEntry) : null;

    if (this.script) {
      if (!script || !script.url.includes(this.script)) return false;
    }
    if (this.scriptId !== null) {
      if (!script || script.id !== this.scriptId) return false;
    }
    if (this.functionName) {
      const fnName = entry.fnName || entry.functionName || '';
      if (!fnName.includes(this.functionName)) return false;
    }
    if (this.file) {
      if (!script || !script.url.includes(this.file)) return false;
    }
    if (this.timeRange) {
      if (entry.time < this.timeRange.start || entry.time > this.timeRange.end)
        return false;
    }
    return true;
  }

  filterIc(entry, processor) {
    const script = processor.getProfileEntryScript(entry.codeEntry);
    return this._matchesCommon(entry, script);
  }

  filterScript(script) {
    if (this.file && !script.url.includes(this.file)) return false;
    if (this.scriptId !== null && script.id !== this.scriptId) return false;
    if (this.script && !script.url.includes(this.script)) return false;
    return true;
  }

  filterFunction(info) {
    if (this.file && !info.script.includes(this.file)) return false;
    if (this.scriptId !== null && info.scriptId !== this.scriptId) return false;

    if (this.line !== undefined) {
      if (!info.sourcePosition || info.sourcePosition.line !== this.line)
        return false;
    }
    if (this.column !== undefined) {
      if (!info.sourcePosition || info.sourcePosition.column !== this.column)
        return false;
    }
    if (this.lineRange) {
      if (!info.sourcePosition ||
          info.sourcePosition.line < this.lineRange.start ||
          info.sourcePosition.line > this.lineRange.end)
        return false;
    }
    return true;
  }

  _matchesCommon(entry, script) {
    if (this.script) {
      if (!script || !script.url.includes(this.script)) return false;
    }
    if (this.scriptId !== null) {
      if (!script || script.id !== this.scriptId) return false;
    }
    if (this.functionName) {
      const fnName = entry.fnName || entry.functionName || '';
      if (!fnName.includes(this.functionName)) return false;
    }
    if (this.file) {
      if (!script || !script.url.includes(this.file)) return false;

      if (this.line !== undefined) {
        if (entry.line !== this.line) return false;
      }
      if (this.column !== undefined) {
        if (entry.column !== this.column) return false;
      }
      if (this.lineRange) {
        if (entry.line < this.lineRange.start ||
            entry.line > this.lineRange.end)
          return false;
      }
    }
    if (this.timeRange) {
      if (entry.time < this.timeRange.start || entry.time > this.timeRange.end)
        return false;
    }
    if (this.map) {
      const entryMap = entry.map?.id || entry.map || entry.mapId || entry.id;
      if (entryMap !== this.map) return false;
    }
    return true;
  }
}

const VALID_CODE_KINDS = Profile.CODE_KIND_NAMES.filter(k => k !== undefined);

// Specific Filter for Code and Function commands
class CodeFilter extends EventFilter {
  constructor() {
    super();
    this.codeKind = null;
  }

  static get help() {
    return EventFilter.help +
        `\n  --code-kind=<kind>   Filter by code kind (valid: ${
               VALID_CODE_KINDS.join(', ')})`;
  }

  parse(args) {
    super.parse(args);
    for (const arg of args) {
      if (!arg.startsWith('--code-kind=')) continue;

      const kind = arg.substring('--code-kind='.length);
      const matchedKind =
          VALID_CODE_KINDS.find(k => k.toLowerCase() === kind.toLowerCase());

      if (!matchedKind) {
        throw new Error(`Invalid code kind: ${kind}. Valid kinds are: ${
            VALID_CODE_KINDS.join(', ')}`);
      }
      this.codeKind = matchedKind;
    }
  }

  matchesCodeKind(entry) {
    if (this.codeKind) {
      return entry.kindName === this.codeKind;
    }
    return true;
  }
}

// Specific Filter for Deopt command
class DeoptFilter extends EventFilter {
  constructor() {
    super();
    this.deoptType = null;
  }

  static get help() {
    return EventFilter.help +
        `\n  --deopt-type=<type>  Filter by deopt type (e.g., deopt-eager, dependency-change)`;
  }

  parse(args) {
    super.parse(args);
    for (const arg of args) {
      if (!arg.startsWith('--deopt-type=')) continue;

      this.deoptType = arg.substring('--deopt-type='.length);
    }
  }

  matchesDeoptType(entry) {
    if (this.deoptType) {
      return entry.type === this.deoptType;
    }
    return true;
  }
}

class IcEventFilter extends EventFilter {
  constructor() {
    super();
    this.icType = null;
    this.key = null;
    this.state = null;
    this.oldStateFilter = null;
    this.newStateFilter = null;
  }

  static get help() {
    return EventFilter.help +
        `\n  --type=<type>        Filter by IC type (e.g., LoadIC, StoreIC)` +
        `\n  --ic-type=<type>     Alias for --type` +
        `\n  --key=<name>        Filter by property key` +
        `\n  --state=<state>      Filter by IC state transition (e.g., 01, 0:1, "1 0", 0-1)`;
  }

  parse(args) {
    super.parse(args);
    for (const arg of args) {
      if (this.parseIcType(arg)) continue;
      if (this.parseKey(arg)) continue;
      if (this.parseState(arg)) continue;
    }
  }

  hasFilters() {
    return super.hasFilters() || this.icType !== null || this.key !== null ||
        this.state !== null || this.oldStateFilter !== null ||
        this.newStateFilter !== null;
  }

  parseIcType(arg) {
    const isType = arg.startsWith('--type=');
    const isIcType = arg.startsWith('--ic-type=');
    if (!isType && !isIcType) return false;
    const prefix = isType ? '--type=' : '--ic-type=';
    this.icType = arg.substring(prefix.length);
    return true;
  }

  parseKey(arg) {
    if (!arg.startsWith('--key=')) return false;
    this.key = arg.substring('--key='.length);
    return true;
  }

  parseState(arg) {
    if (!arg.startsWith('--state=')) return false;
    const stateVal = arg.substring('--state='.length);

    const match =
        stateVal.match(/^([a-zA-Z0-9^.])\s*[:.\s-→]*\s*([a-zA-Z0-9^.])$/);
    if (match) {
      this.oldStateFilter = match[1];
      this.newStateFilter = match[2];
    } else {
      this.state = stateVal;
    }
    return true;
  }

  matchesIc(entry) {
    if (this.icType) {
      if (entry.type !== this.icType) return false;
    }
    if (this.key) {
      if (entry.key !== this.key) return false;
    }
    if (this.oldStateFilter) {
      if (entry.oldState !== this.oldStateFilter) return false;
    }
    if (this.newStateFilter) {
      if (entry.newState !== this.newStateFilter) return false;
    }
    if (this.state) {
      const entryState =
          entry.state || (entry.oldState + ' → ' + entry.newState);
      if (!entryState.includes(this.state)) return false;
    }
    return true;
  }
}

// Class Hierarchy for Commands
class Command {
  constructor(processor = null) {
    this.processor = processor;
    this.filter = new EventFilter();
    this.format = 'json';  // Default to json
    this.details = false;
    this.sortField = null;
    this.sortAscending = false;  // Default to descending
    this.limit = 0;
    this.limitStart = undefined;
    this.limitEnd = undefined;
    this.limitLast = 0;
    this.verbose = false;
  }

  static get help() {
    throw new Error('Subclass responsibility');
  }

  execute(args) {
    // Assume parseArgs and filter.parse have been called!
    let results = this.run(args);

    if (Array.isArray(results)) {
      this.applySort(results);
      this.applyLimit(results);

      // Reduce to basic keys if not detailed and format is JSON
      if (!this.details && this.format === 'json') {
        results = this.reduceToBasic(results);
      }
    }

    if (this.format === 'json') {
      print(JSON.stringify(results, null, 2));
    } else if (this.format === 'count') {
      if (Array.isArray(results)) {
        print(results.length);
      } else if (typeof results === 'object') {
        print(Object.keys(results).length);
      } else {
        print(1);
      }
    } else {
      this.printText(results);
    }
  }

  parseArgs(args) {
    this.filter.parse(args);
    for (const arg of args) {
      if (arg.startsWith('--sort=')) {
        this.parseSort(arg);
        continue;
      }
      if (arg.startsWith('--limit=') || arg.startsWith('-n=')) {
        this.parseLimit(arg);
        continue;
      }
      if (arg.startsWith('--format=')) {
        this.parseFormat(arg);
        continue;
      }
      if (arg === '--count') {
        this.parseCount(arg);
        continue;
      }
      if (arg === '--details' || arg === '--detail') {
        this.details = true;
        continue;
      }

      if (arg === '--verbose') {
        this.verbose = true;
        continue;
      }
    }
  }

  parseSort(arg) {
    const sortVal = arg.substring('--sort='.length);
    if (sortVal.startsWith('+')) {
      this.sortField = sortVal.substring(1);
      this.sortAscending = true;
    } else if (sortVal.startsWith('-')) {
      this.sortField = sortVal.substring(1);
      this.sortAscending = false;
    } else {
      this.sortField = sortVal;
      this.sortAscending = false;  // Default to descending
    }
  }

  parseLimit(arg) {
    const limitVal = arg.substring(arg.indexOf('=') + 1);
    const match = limitVal.match(/^(-?\d+)[:.]{1,3}(-?\d+)$/);
    if (match) {
      this.limitStart = parseInt(match[1]);
      this.limitEnd = parseInt(match[2]);
    } else if (limitVal.startsWith('-')) {
      this.limitLast = parseInt(limitVal.substring(1));
    } else {
      this.limit = parseInt(limitVal);
    }
  }

  parseFormat(arg) {
    const formatVal = arg.substring('--format='.length);
    if (this.format !== 'json' && this.format !== formatVal) {
      throw new Error('Flags --format and --count are mutually exclusive!');
    }
    this.format = formatVal;
  }

  parseCount(arg) {
    if (this.format !== 'json' && this.format !== 'count') {
      throw new Error('Flags --format and --count are mutually exclusive!');
    }
    this.format = 'count';
  }

  applySort(results) {
    if (!this.sortField) return;
    const pathParts = this.sortField.split('.');
    const ascending = this.sortAscending;
    let getSortKey;

    if (pathParts.length === 1) {
      getSortKey = (obj) => obj[this.sortField];
    } else {
      // Allow sorting by nested properties: --sort=stats.code
      getSortKey = (obj) =>
          pathParts.reduce((acc, part) => (acc ? acc[part] : undefined), obj);
    }

    results.sort((a, b) => {
      const valA = getSortKey(a);
      const valB = getSortKey(b);
      if (valA === undefined || valB === undefined) return 0;
      if (valA < valB) return ascending ? -1 : 1;
      if (valA > valB) return ascending ? 1 : -1;
      return 0;
    });
  }

  applyLimit(results) {
    if (this.limitLast > 0) {
      results.splice(0, results.length - this.limitLast);
    } else if (this.limitStart !== undefined && this.limitEnd !== undefined) {
      const start = this.limitStart;
      const end = this.limitEnd;
      if (end < results.length) {
        results.splice(end);
      }
      if (start > 0) {
        results.splice(0, start);
      }
    } else if (this.limit > 0) {
      results.splice(this.limit);
    }
  }

  reduceToBasic(results) {
    const keys = this.constructor.basicKeys;
    if (!keys) return results;

    return results.map(entry => {
      const reduced = {__proto__: null};
      keys.forEach(key => {
        reduced[key] = entry[key];
      });
      return reduced;
    });
  }

  async processStdInLog(rawArgs) {
    const processor = new Processor();
    processor.verbose = this.verbose;

    printErr('Processing log from stdin...');

    let line;
    let count = 0;
    while (line = readline()) {
      await processor.processLogLine(line);
      count++;
      if (count % 100000 === 0) {
        printErr(`Processed ${count} lines...`);
      }
    }

    await processor.finalize();

    printErr('Processing complete.');

    this.processor = processor;
    this.execute(rawArgs);
  }

  run(args) {
    throw new Error('Subclass responsibility');
  }

  printText(results) {
    if (!Array.isArray(results)) {
      throw new Error('Generic printText only supports arrays of results.');
    }
    const fields = this.constructor.basicKeys;
    if (!fields) {
      throw new Error(`Subclass ${
          this.constructor
              .name} must define static get basicKeys to use generic printText`);
    }

    print(`\n--- ${this.constructor.name.replace('Command', 's')} ---`);
    results.forEach(entry => {
      // 1. Print the basic line
      print(this.formatTextBasicLine(entry));

      // 2. Print details if requested
      if (this.details) {
        this.printTextDetails(entry);
      }
    });
  }

  formatTextBasicLine(entry) {
    const keys = this.constructor.basicKeys;
    return keys.map(key => `${key}: ${this.formatTextValue(entry[key])}`)
        .join(', ');
  }

  formatTextValue(val) {
    if (val === undefined || val === null) return '<none>';

    if (typeof val === 'object' && !Array.isArray(val)) {
      // Handle position-like objects duck-typed
      if (val.line !== undefined && val.column !== undefined) {
        return `${val.line}:${val.column}`;
      }
      // Handle stats-like maps (like the 'code' field in FunctionCommand)
      return `{ ${
          Object.entries(val).map(([k, v]) => `${k} x ${v}`).join(', ')} }`;
    }
    return val;
  }

  printTextDetails(entry) {
    // Subclasses can override printCustomTextDetails(entry)
    if (typeof this.printCustomTextDetails === 'function') {
      this.printCustomTextDetails(entry);
      return;
    }

    // Fallback to safe defaults for common shapes
    if (entry.sourceLines || entry.source) {
      const lines = entry.sourceLines || entry.source;
      print(`  source:`);
      lines.forEach(l => print(`    ${l}`));
    }
  }
}

const STATS_PADDING = 22;

class StatsCommand extends Command {
  static get commandName() {
    return 'stats';
  }
  static get aliases() {
    return ['stat'];
  }

  static get help() {
    return 'Print statistics of all events found in the log.\n\n' +
        'Prints aggregated statistics of all events found in the log, ' +
        'including the total number of loaded scripts, compiled functions, ' +
        'code objects created, Inline Cache (IC) events, maps, ' +
        'deoptimizations, profiler ticks, and timers.\n\n' + EventFilter.help;
  }

  run(args) {
    if (!this.filter.hasFilters()) {
      let functions = 0;
      this.processor.codeTimeline.forEach(entry => {
        if (!entry.isScript && !entry.isBuiltinKind) {
          functions++;
        }
      });

      return {
        scripts: this.processor.scripts.length,
        functions: functions,
        codeObjects: this.processor.codeTimeline.length,
        ics: this.processor.icTimeline.length,
        maps: this.processor.mapTimeline.length,
        deopts: this.processor.deoptTimeline.length,
        ticks: this.processor.tickTimeline.length,
        timers: this.processor.timerTimeline.length
      };
    }

    const stats = {
      scripts: 0,
      functions: 0,
      codeObjects: 0,
      ics: 0,
      maps: 0,
      deopts: 0,
      ticks: 0,
      timers: 0
    };

    this.processor.scripts.forEach(script => {
      if (this.filter.filterScript(script)) stats.scripts++;
    });

    this.processor.codeTimeline.forEach(entry => {
      if (!this.filter.filterCode(entry, this.processor)) return;
      stats.codeObjects++;
      if (!entry.isScript && !entry.isBuiltinKind) {
        stats.functions++;
      }
    });

    this.processor.icTimeline.forEach(entry => {
      if (this.filter.filterIc(entry, this.processor)) stats.ics++;
    });

    this.processor.mapTimeline.forEach(entry => {
      if (this.filter._matchesCommon(entry, null)) stats.maps++;
    });

    this.processor.deoptTimeline.forEach(entry => {
      const script = this.processor.getProfileEntryScript(entry.entry);
      if (this.filter._matchesCommon(entry, script)) stats.deopts++;
    });

    this.processor.tickTimeline.forEach(entry => {
      stats.ticks++;
    });

    this.processor.timerTimeline.forEach(entry => {
      stats.timers++;
    });

    return stats;
  }

  printText(stats) {
    print('\n--- Event Statistics ---');
    print(`scripts:`.padEnd(STATS_PADDING) + stats.scripts);
    print(`functions (compiled):`.padEnd(STATS_PADDING) + stats.functions);
    print(`code objects:`.padEnd(STATS_PADDING) + stats.codeObjects);
    print(`ics:`.padEnd(STATS_PADDING) + stats.ics);
    print(`maps:`.padEnd(STATS_PADDING) + stats.maps);
    print(`deopts:`.padEnd(STATS_PADDING) + stats.deopts);
    print(`ticks:`.padEnd(STATS_PADDING) + stats.ticks);
    print(`timers:`.padEnd(STATS_PADDING) + stats.timers);
  }
}

class IcListCommand extends Command {
  constructor(processor = null) {
    super(processor);
    this.filter = new IcEventFilter();
  }

  static get commandName() {
    return 'ic-list';
  }

  static get aliases() {
    return [];
  }

  static get help() {
    return 'List detailed information about individual Inline Cache events.\n\n' +
        'ICs are used by V8 to optimize property accesses. The output shows ' +
        'the time of the event, the type of operation, the function name, ' +
        'the state transition (e.g., monomorphic to polymorphic), the key ' +
        'accessed, and the map ID.\n\n' + IcEventFilter.help;
  }

  static get basicKeys() {
    return ['time', 'type', 'functionName', 'state', 'key', 'map'];
  }

  run(args) {
    const events = [];

    this.processor.icTimeline.forEach(entry => {
      if (!this.filter.filterIc(entry, this.processor)) return;
      if (!this.filter.matchesIc(entry)) return;
      events.push(entry.toDetailJSON());
    });

    return events;
  }
}

class ScriptCommand extends Command {
  static get commandName() {
    return 'script';
  }

  static get aliases() {
    return ['scripts'];
  }

  static get help() {
    return 'List all loaded scripts.\n\n' +
        'The output shows the unique script ID, the URL or filename, and ' +
        'the size of the source code in characters. In details mode, it ' +
        'also lists the source code lines.\n\n' + EventFilter.help;
  }

  static get basicKeys() {
    return ['id', 'name', 'size', 'stats'];
  }

  run(args) {
    const results = [];
    this.processor.scripts.forEach(script => {
      if (!this.filter.filterScript(script)) return;
      const detail = script.toDetailJSON();

      const stats = {__proto__: null};
      script.entries.forEach(entry => {
        const key = entry.typeName;
        stats[key] = (stats[key] || 0) + 1;
      });

      detail.stats = stats;
      results.push(detail);
    });

    return results;
  }
}

class CodeCommand extends Command {
  static get commandName() {
    return 'code';
  }

  static get aliases() {
    return ['codes'];
  }

  constructor(processor = null) {
    super(processor);
    this.filter = new CodeFilter();
  }

  static get help() {
    return 'List all created code objects.\n\n' +
        'Lists all code objects created by V8 (e.g., full code, bytecode, ' +
        'handlers). The output shows the time of creation, the event type, ' +
        'the optimization tier (kind), the function or handler name, the ' +
        'size in bytes, and the script ID.\n\n' + CodeFilter.help;
  }

  static get basicKeys() {
    return ['time', 'type', 'kindName', 'name', 'size', 'scriptId'];
  }

  run(args) {
    const results = [];
    this.processor.codeTimeline.forEach(entry => {
      if (!this.filter.filterCodeBasic(entry, this.processor)) return;
      if (!this.filter.matchesCodeKind(entry)) return;

      // Apply line/col filters if active
      if (this.filter.line !== undefined || this.filter.column !== undefined ||
          this.filter.lineRange) {
        if (!entry.entry || !entry.entry.sfi) return;
        const sfi = entry.entry.sfi;
        const name = sfi.getName();
        const match = name.match(/:(\d+):(\d+)$/);
        if (!match) return;

        const line = parseInt(match[1]);
        const column = parseInt(match[2]);

        if (this.filter.line !== undefined && line !== this.filter.line) return;
        if (this.filter.column !== undefined && column !== this.filter.column)
          return;
        if (this.filter.lineRange) {
          if (line < this.filter.lineRange.start ||
              line > this.filter.lineRange.end)
            return;
        }
      }

      results.push(entry.toDetailJSON());
    });

    return results;
  }

  printCustomTextDetails(entry) {
    if (entry.deopts) {
      print(`  deopts:`);
      entry.deopts.forEach(
          d => print(`    reason: ${d.reason}, location: ${d.location}`));
    }
  }
}

class GroupStatsCommand extends Command {
  static get groupKeys() {
    throw new Error('Subclass responsibility');
  }

  static get basicKeys() {
    return ['count', ...this.groupKeys];
  }

  get timeline() {
    throw new Error('Subclass responsibility');
  }

  filterEntry(entry) {
    return true;
  }

  getEntryDetails(entry) {
    const detail = entry.toDetailJSON();
    if (!detail.location && detail.sourcePosition?.file) {
      detail.location = detail.sourcePosition.file;
    }
    if (!detail.location) {
      detail.location = '<unknown>';
    }

    detail.script = detail.script || entry.script?.url || '<unknown>';
    detail.scriptId ??= entry.script?.id ?? '<unknown>';

    return detail;
  }

  run(args) {
    const groupKeys = this.constructor.groupKeys;
    let groupBy = groupKeys[0];
    for (const arg of args) {
      for (const flagName of ['--group-by=', '--group=', '-g=']) {
        if (arg.startsWith(flagName)) {
          groupBy = arg.substring(flagName.length);
        }
      }
    }

    const groupByKeys = [...new Set(groupBy.split(',').map(s => s.trim()))];

    groupByKeys.forEach(k => {
      if (!groupKeys.includes(k)) {
        throw new Error(`Invalid group-by key: ${k}. Valid keys are: ${
            groupKeys.join(', ')}`);
      }
    });

    const stats = new Map();

    this.timeline.forEach(entry => {
      if (!this.filterEntry(entry)) return;

      const detail = this.getEntryDetails(entry);

      const groupValues = groupByKeys.map(k => detail[k] || '<unknown>');
      const groupKey = JSON.stringify(groupValues);

      let groupStats = stats.get(groupKey);
      if (!groupStats) {
        groupStats = {__proto__: null, count: 0};
        groupByKeys.forEach((k, i) => {
          groupStats[k] = groupValues[i];
        });
        groupKeys.forEach(k => {
          if (!groupByKeys.includes(k)) {
            groupStats[k] = {__proto__: null};
          }
        });
        stats.set(groupKey, groupStats);
      }

      groupStats.count++;

      groupKeys.forEach(k => {
        if (!groupByKeys.includes(k)) {
          const val = detail[k] || '<unknown>';
          groupStats[k][val] = (groupStats[k][val] || 0) + 1;
        }
      });
    });

    let results = Array.from(stats.values());

    // Default sort by count descending if no sort specified
    if (!this.sortField) {
      results.sort((a, b) => b.count - a.count);
    }

    // Apply details logic
    if (!this.details) {
      results = results.map(group => {
        const reducedGroup = {__proto__: null, count: group.count};
        groupByKeys.forEach(k => {
          reducedGroup[k] = group[k];
        });

        groupKeys.forEach(k => {
          if (!groupByKeys.includes(k)) {
            const subMap = group[k];
            const sortedEntries =
                Object.entries(subMap)
                    .sort((a, b) => b[1] - a[1])
                    .slice(0, this.constructor.NON_DETAILS_LIMIT || 10);

            const reducedSubMap = {__proto__: null};
            sortedEntries.forEach(([key, val]) => {
              reducedSubMap[key] = val;
            });
            reducedGroup[k] = reducedSubMap;
          }
        });
        return reducedGroup;
      });
    }

    return results;
  }
}

class DeoptCommand extends GroupStatsCommand {
  static get commandName() {
    return 'deopt';
  }

  static get aliases() {
    return ['deopts'];
  }

  constructor(processor = null) {
    super(processor);
    this.filter = new DeoptFilter();
  }

  static get help() {
    return 'Print statistics of deoptimizations.\n\n' +
        'Aggregates deoptimizations by specified keys (default: type) ' +
        'and shows counts for other keys.\n\n' + EventFilter.help + '\n' +
        `  --group-by=<key1,key2,...> Keys to group by (valid: ${
               this.groupKeys.join(', ')})`;
  }

  static get groupKeys() {
    return ['type', 'reason', 'location', 'functionName', 'scriptId', 'script'];
  }
  static NON_DETAILS_LIMIT = 10;

  get timeline() {
    return this.processor.deoptTimeline;
  }

  filterEntry(entry) {
    const script = this.processor.getProfileEntryScript(entry.entry);
    return this.filter._matchesCommon(entry, script) &&
        this.filter.matchesDeoptType(entry);
  }
}

class FunctionCommand extends Command {
  static get commandName() {
    return 'function';
  }

  static get aliases() {
    return ['functions', 'fn', 'fns'];
  }

  constructor(processor = null) {
    super(processor);
    this.filter = new CodeFilter();
  }

  static get help() {
    return 'List all compiled functions.\n\n' +
        'A function can have multiple code variants (e.g., bytecode and ' +
        'optimized machine code). The output shows the pure function name, ' +
        'the script ID, the number of variants, and a breakdown of the ' +
        'code types created for it.\n\n' + CodeFilter.help;
  }

  static get basicKeys() {
    return ['name', 'scriptId', 'variants', 'code'];
  }

  run(args) {
    const uniqueSFIs = new Map();
    this.processor.codeTimeline.forEach(entry => {
      if (!this.filter.filterCodeBasic(entry, this.processor)) return;
      if (!this.filter.matchesCodeKind(entry)) return;
      if (!entry.entry || !entry.entry.sfi) return;

      const sfi = entry.entry.sfi;
      if (uniqueSFIs.has(sfi)) return;

      const script = this.processor.getProfileEntryScript(entry.entry);

      const info = sfi.toDetailJSON(script);

      if (!this.filter.filterFunction(info)) return;

      uniqueSFIs.set(sfi, info);
    });

    const results = [];
    uniqueSFIs.forEach((info, sfi) => {
      results.push(info);
    });

    return results;
  }
}

class IcCommand extends GroupStatsCommand {
  constructor(processor = null) {
    super(processor);
    this.filter = new IcEventFilter();
  }

  static get commandName() {
    return 'ic';
  }

  static get aliases() {
    return ['ics', 'ic-stats'];
  }

  static get help() {
    return 'Print statistics of Inline Cache events.\n\n' +
        'Aggregates IC events by specified keys (default: functionName) ' +
        'and shows counts for other keys.\n\n' + IcEventFilter.help + '\n' +
        `  --group-by=<key1,key2,...> Keys to group by (valid: ${
               this.groupKeys.join(', ')})`;
  }

  static get groupKeys() {
    return [
      'type', 'state', 'functionName', 'key', 'map', 'script', 'location'
    ];
  }
  static NON_DETAILS_LIMIT = 10;

  get timeline() {
    return this.processor.icTimeline;
  }

  filterEntry(entry) {
    return this.filter.filterIc(entry, this.processor) &&
        this.filter.matchesIc(entry);
  }
}

class ProfileCommand extends Command {
  static get commandName() {
    return 'profile';
  }

  static get aliases() {
    return ['ticks', 'perf', 'prof'];
  }

  static get help() {
    return 'Print profile of filtered ticks.\n\n' +
        'Shows a profile similar to tick-processor but for filtered ticks only.\n\n' +
        EventFilter.help;
  }

  run(args) {
    const CallTree = this.processor.profile.topDownTree_.constructor;
    const tree = new CallTree();

    let totalTicks = 0;
    const flatProfile = new Map();

    this.processor.tickTimeline.forEach(tick => {
      if (!this.filterTick(tick)) return;

      totalTicks++;

      // Flat profile collection
      const topEntry = tick.stack[0];
      let name = 'UNKNOWN';
      let type = 'Other';

      if (typeof topEntry === 'number' || typeof topEntry === 'bigint') {
        name = 'UNKNOWN';
      } else if (topEntry) {
        name = topEntry.getName();
        if (topEntry.type === 'SHARED_LIB')
          type = 'LIB';
        else if (topEntry.type === 'CPP')
          type = 'CPP';
        else if (
            topEntry.isJSFunction?.() || topEntry.type === 'LazyCompile' ||
            topEntry.type === 'Function')
          type = 'JS';
      }

      const stat = flatProfile.get(name) || {ticks: 0, type: type, name: name};
      stat.ticks++;
      flatProfile.set(name, stat);

      // Tree profile collection
      const nameStack = [];
      for (const entry of tick.stack) {
        if (typeof entry === 'number' || typeof entry === 'bigint') {
          nameStack.push('UNKNOWN');
        } else if (entry) {
          const entryName = entry.getName();
          if (!this.processor.profile.skipThisFunction(entryName)) {
            nameStack.push(entryName);
          }
        }
      }
      tree.addPath(nameStack);
    });

    tree.computeTotalWeights();
    return {tree, flatProfile, totalTicks};
  }

  filterTick(tick) {
    if (this.filter.timeRange) {
      if (tick.startTime < this.filter.timeRange.start ||
          tick.startTime > this.filter.timeRange.end) {
        return false;
      }
    }
    if (this.filter.script || this.filter.scriptId !== null ||
        this.filter.functionName || this.filter.file) {
      for (const entry of tick.stack) {
        if (typeof entry === 'number' || typeof entry === 'bigint') continue;
        if (!entry) continue;

        const script = this.processor.getProfileEntryScript(entry);
        let matches = true;

        if (this.filter.script) {
          if (!script || !script.url.includes(this.filter.script))
            matches = false;
        }
        if (this.filter.scriptId !== null) {
          if (!script || script.id !== this.filter.scriptId) matches = false;
        }
        if (this.filter.functionName) {
          if (!entry.getName().includes(this.filter.functionName))
            matches = false;
        }
        if (this.filter.file) {
          if (!script || !script.url.includes(this.filter.file))
            matches = false;
        }

        if (matches) return true;
      }
      return false;
    }
    return true;
  }

  execute(args) {
    const results = this.run(args);
    if (this.format === 'json') {
      const jsonTree = this.nodeToJSON(results.tree.getRoot());
      const jsonFlat = Array.from(results.flatProfile.values());
      print(JSON.stringify(
          {
            tree: jsonTree,
            flatProfile: jsonFlat,
            totalTicks: results.totalTicks
          },
          null, 2));
    } else if (this.format === 'count') {
      print(results.totalTicks);
    } else {
      this.printText(results);
    }
  }

  nodeToJSON(node) {
    const children = {};
    for (const [label, child] of Object.entries(node.children)) {
      children[label] = this.nodeToJSON(child);
    }
    return {
      label: node.label,
      selfWeight: node.selfWeight,
      totalWeight: node.totalWeight,
      children: children
    };
  }

  printText(result) {
    const {tree, flatProfile, totalTicks} = result;

    if (totalTicks === 0) {
      print('No ticks found.');
      return;
    }

    // Count library ticks
    let libraryTicks = 0;
    flatProfile.forEach(stat => {
      if (stat.type === 'LIB') libraryTicks += stat.ticks;
    });
    const nonLibraryTicks = totalTicks - libraryTicks;

    const state = {count: 0, max: this.limit > 0 ? this.limit : Infinity};

    this.printSection(
        'Shared libraries', stat => stat.type === 'LIB', flatProfile,
        totalTicks, nonLibraryTicks, state);
    this.printSection(
        'JavaScript', stat => stat.type === 'JS', flatProfile, totalTicks,
        nonLibraryTicks, state);
    this.printSection(
        'C++', stat => stat.type === 'CPP', flatProfile, totalTicks,
        nonLibraryTicks, state);

    const allTicks = this.processor.tickTimeline.length;
    const totalPct = (totalTicks * 100 / allTicks).toFixed(1);
    print(`\n--- Filtered Ticks Profile (${totalTicks} ticks, ${
        totalPct}% of total ${allTicks}) ---`);

    this.printNode(tree.getRoot(), 0, totalTicks, state);
  }

  printSection(
      title, typeFilter, flatProfile, totalTicks, nonLibraryTicks, state) {
    if (state.count >= state.max) return;

    const entries = Array.from(flatProfile.values())
                        .filter(stat => typeFilter(stat))
                        .sort((a, b) => b.ticks - a.ticks);

    if (entries.length === 0) return;

    print(`\n [${title}]:`);
    print('   ticks  total  nonlib   name');

    for (const stat of entries) {
      if (state.count >= state.max) break;

      const pct = (stat.ticks * 100 / totalTicks).toFixed(1);
      const nonLibPct = nonLibraryTicks > 0 && stat.type !== 'LIB' ?
          `${
              (stat.ticks * 100 / nonLibraryTicks)
                  .toFixed(1)
                  .toString()
                  .padStart(5)}%` :
          '      ';
      print(`${`  ${stat.ticks.toString().padStart(5)}  `}${
          pct.toString().padStart(5)}%  ${nonLibPct}   ${stat.name}`);
      state.count++;
    }
  }

  printNode(node, indent, totalTicks, state) {
    if (state && state.count >= state.max) return;
    if (this.limit > 0 && indent / 2 >= this.limit) return;

    const indentStr = ''.padStart(indent);
    const pct = (node.totalWeight * 100 / totalTicks).toFixed(1);

    if (node.label !== '') {
      print(`${`  ${node.totalWeight.toString().padStart(5)}  `}${
          pct.toString().padStart(5)}%  ${indentStr}${node.label}`);
      if (state) state.count++;
    }

    const children = Object.values(node.children)
                         .sort((a, b) => b.totalWeight - a.totalWeight);

    if (this.limit > 0) {
      children.splice(this.limit);
    }

    for (const child of children) {
      if (child.totalWeight * 100 / totalTicks < 1.0) continue;
      this.printNode(
          child, node.label !== '' ? indent + 2 : indent, totalTicks, state);
    }
  }
}

class MapCommand extends GroupStatsCommand {
  static get commandName() {
    return 'map';
  }

  static get aliases() {
    return ['maps', 'map-stats'];
  }

  static get help() {
    return 'Print statistics of Map events.\n\n' +
        'Aggregates Map events by specified keys (default: type) ' +
        'and shows counts for other keys.\n\n' + EventFilter.help + '\n' +
        `  --group-by=<key1,key2,...> Keys to group by (valid: ${
               this.groupKeys.join(', ')})`;
  }

  static get groupKeys() {
    return ['type', 'reason', 'property', 'functionName', 'script', 'location'];
  }
  static NON_DETAILS_LIMIT = 10;

  get timeline() {
    return this.processor.mapTimeline;
  }

  filterEntry(entry) {
    return this.filter._matchesCommon(entry, null);
  }

  getEntryDetails(entry) {
    return entry.toDetailJSON();
  }
}

const COMMANDS = Object.freeze([
  StatsCommand,
  ScriptCommand,
  CodeCommand,
  FunctionCommand,
  DeoptCommand,
  IcCommand,
  IcListCommand,
  ProfileCommand,
  MapCommand,
]);

const COMMANDS_LOOKUP = {
  __proto__: null
};
for (const cls of COMMANDS) {
  COMMANDS_LOOKUP[cls.commandName] = cls;
  for (const alias of cls.aliases) {
    COMMANDS_LOOKUP[alias] = cls;
  }
}
Object.freeze(COMMANDS_LOOKUP);

function handleHelp(commandName, args) {
  const CommandClass = COMMANDS_LOOKUP[commandName];
  if (CommandClass) {
    print(CommandClass.help);
    return;
  }

  print('Usage: tools/logviewer <command> <v8.log> [options]');
  print('\nCommands:');
  for (const cls of COMMANDS) {
    print(`  ${cls.commandName.padEnd(10)} ${cls.help.split('\n')[0]}`);
  }
  print('\n' + EventFilter.help);
}

export async function main(args = []) {
  const hasHelp = args.includes('--help') || args.includes('help');

  if (hasHelp) {
    // Find the first argument that is a valid command name
    const commandName = args.find(a => COMMANDS_LOOKUP[a]);
    handleHelp(commandName, args);
    return;
  }

  const commandName = args[0];
  if (!commandName) {
    throw new Error(`No subcommand provided. Valid commands are: ${
        COMMANDS.map(c => c.commandName).join(', ')}`);
  }

  const CommandClass = COMMANDS_LOOKUP[commandName];
  if (!CommandClass) {
    throw new Error(
        `Invalid subcommand: '${commandName}'. Valid commands are: ${
            COMMANDS.map(c => c.commandName).join(', ')}`);
  }

  const cmd = new CommandClass();
  const rawArgs = Array.prototype.slice.call(args, 1);
  cmd.parseArgs(rawArgs);
  await cmd.processStdInLog(rawArgs);
}

if (globalThis.arguments) {
  main(globalThis.arguments);
}

export {
  StatsCommand,
  ScriptCommand,
  CodeCommand,
  DeoptCommand,
  FunctionCommand,
  IcCommand,
  IcListCommand,
  ProfileCommand,
  COMMANDS,
  COMMANDS_LOOKUP
};