// Copyright 2019 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.

// TODO(v8:10266): Figure out why this fails on tsan with --always-opt.
// Flags: --no-always-opt --no-turbo-inline-js-wasm-calls

let {session, contextGroup, Protocol} = InspectorTest.start(
    'Test that console profiles contain wasm function names.');

utils.load('test/mjsunit/wasm/wasm-module-builder.js');

// Build module bytes, including a sentinel such that the module will not be
// reused from the cache.
let sentinel = 0;
function buildModuleBytes() {
  ++sentinel;
  InspectorTest.log(`Building wasm module with sentinel ${sentinel}.`);
  // Add fibonacci function, calling back and forth between JS and Wasm to also
  // check for the occurrence of the wrappers.
  var builder = new WasmModuleBuilder();
  const imp_index = builder.addImport('q', 'f', kSig_i_i);
  builder.addFunction('fib', kSig_i_i)
      .addBody([
        ...wasmI32Const(sentinel), kExprDrop,
        kExprLocalGet, 0,
        kExprLocalGet, 0,
        kExprI32Const, 2,
        kExprI32LeS,  // i < 2 ?
        kExprBrIf, 0, // --> return i
        kExprI32Const, 1, kExprI32Sub,  // i - 1
        kExprCallFunction, imp_index, // imp(i - 1)
        kExprLocalGet, 0, kExprI32Const, 2, kExprI32Sub,  // i - 2
        kExprCallFunction, imp_index, // imp(i - 2)
        kExprI32Add
      ])
      .exportFunc();
  return builder.toArray();
}

function compile(bytes) {
  let buffer = new ArrayBuffer(bytes.length);
  let view = new Uint8Array(buffer);
  for (var i = 0; i < bytes.length; i++) {
    view[i] = bytes[i] | 0;
  }
  let module = new WebAssembly.Module(buffer);
  let fib = undefined;
  function imp(i) { return fib(i); }
  let instance = new WebAssembly.Instance(module, {q: {f: imp}});
  fib = instance.exports.fib;
  return instance;
}

function checkError(message) {
  if (!message.error) return;
  InspectorTest.log('Error: ');
  InspectorTest.logMessage(message);
  InspectorTest.completeTest();
}

let found_good_profile = false;
let found_wasm_script_id;
let wasm_position;
let finished_profiles = 0;
Protocol.Profiler.onConsoleProfileFinished(e => {
  ++finished_profiles;
  let nodes = e.params.profile.nodes;
  let function_names = nodes.map(n => n.callFrame.functionName);
  // Enable this line for debugging:
  // InspectorTest.log(function_names.join(', '));
  // Check for at least one full cycle of
  // fib -> wasm-to-js -> imp -> js-to-wasm -> fib.
  // There are two different kinds of js-to-wasm-wrappers, so there are two
  // possible positive traces.
  const expected = [
    ['fib'], ['wasm-to-js:i:i'], ['imp'],
    ['GenericJSToWasmWrapper', 'js-to-wasm:i:i'], ['fib']
  ];
  for (let i = 0; i <= function_names.length - expected.length; ++i) {
    if (expected.every((val, idx) => val.includes(function_names[i + idx]))) {
      found_good_profile = true;
      let wasm_frame = nodes[i].callFrame;
      found_wasm_script_id = wasm_frame.scriptId != 0;
      wasm_position = `${wasm_frame.url}@${wasm_frame.lineNumber}:${
          wasm_frame.columnNumber}`;
    }
  }
});

async function runFibUntilProfileFound() {
  InspectorTest.log(
      'Running fib with increasing input until it shows up in the profile.');
  found_good_profile = false;
  finished_profiles = 0;
  for (let i = 1; !found_good_profile; ++i) {
    checkError(await Protocol.Runtime.evaluate(
        {expression: 'console.profile(\'profile\');'}));
    checkError(await Protocol.Runtime.evaluate(
        {expression: 'globalThis.instance.exports.fib(' + i + ');'}));
    checkError(await Protocol.Runtime.evaluate(
        {expression: 'console.profileEnd(\'profile\');'}));
    if (finished_profiles != i) {
      InspectorTest.log(
          'Missing consoleProfileFinished message (expected ' + i + ', got ' +
          finished_profiles + ')');
    }
  }
  InspectorTest.log('Found expected functions in profile.');
  InspectorTest.log(
      'Wasm script id is ' + (found_wasm_script_id ? 'set.' : 'NOT SET.'));
  InspectorTest.log('Wasm position: ' + wasm_position);
}

async function compileWasm() {
  InspectorTest.log('Compiling wasm.');
  checkError(await Protocol.Runtime.evaluate({
    expression: `globalThis.instance = (${compile})(${
        JSON.stringify(buildModuleBytes())});`
  }));
}

async function testEnableProfilerEarly() {
  InspectorTest.log(arguments.callee.name);
  checkError(await Protocol.Profiler.enable());
  checkError(await Protocol.Profiler.start());
  await compileWasm();
  await runFibUntilProfileFound();
  checkError(await Protocol.Profiler.disable());
}

async function testEnableProfilerLate() {
  InspectorTest.log(arguments.callee.name);
  await compileWasm();
  checkError(await Protocol.Profiler.enable());
  checkError(await Protocol.Profiler.start());
  await runFibUntilProfileFound();
  checkError(await Protocol.Profiler.disable());
}

async function testEnableProfilerAfterDebugger() {
  InspectorTest.log(arguments.callee.name);
  checkError(await Protocol.Debugger.enable());
  await compileWasm();
  checkError(await Protocol.Profiler.enable());
  checkError(await Protocol.Profiler.start());
  await runFibUntilProfileFound();
  checkError(await Protocol.Profiler.disable());
  checkError(await Protocol.Debugger.disable());
}

async function testEnableProfilerBeforeDebugger() {
  InspectorTest.log(arguments.callee.name);
  await compileWasm();
  await Protocol.Profiler.enable();
  await Protocol.Debugger.enable();
  checkError(await Protocol.Profiler.start());
  await runFibUntilProfileFound();
  await Protocol.Debugger.disable();
  await Protocol.Profiler.disable();
}

(async function test() {
  try {
    await testEnableProfilerEarly();
    await testEnableProfilerLate();
    await testEnableProfilerAfterDebugger();
    await testEnableProfilerBeforeDebugger();
  } catch (e) {
    InspectorTest.log('caught: ' + e);
  }
})().catch(e => InspectorTest.log('caught: ' + e))
    .finally(InspectorTest.completeTest);