// META: title=test parallel WebNN API dispatch calls
// META: global=window,worker
// META: variant=?cpu
// META: variant=?gpu
// META: variant=?npu
// META: script=../resources/utils.js
// META: timeout=long

'use strict';

// https://www.w3.org/TR/webnn/#api-mlcontext-dispatch

let mlContext;


// Skip tests if WebNN is unimplemented.
promise_setup(async () => {
  assert_implements(navigator.ml, 'missing navigator.ml');
  mlContext = await navigator.ml.createContext(contextOptions);
});

function buildMulGraph(context, operandDescriptor, multiplier) {
  // Construct a simple graph: A = B * `multiplier`.
  const builder = new MLGraphBuilder(context);
  const inputOperand = builder.input('input', operandDescriptor);
  const constantOperand =
      builder.constant(operandDescriptor, Float32Array.from([multiplier]));
  const outputOperand = builder.mul(inputOperand, constantOperand);
  return builder.build({'output': outputOperand});
}

promise_test(async () => {
  const operandDescriptor = {
    dataType: 'float32',
    shape: [1],
    readable: true,
    writable: true,
  };

  const [mlGraph, inputTensor1, inputTensor2, outputTensor] =
      await Promise.all([
        buildMulGraph(mlContext, operandDescriptor, 2),
        mlContext.createTensor(operandDescriptor),
        mlContext.createTensor(operandDescriptor),
        mlContext.createTensor(operandDescriptor)
      ]);

  mlContext.writeTensor(inputTensor1, Float32Array.from([1]));
  mlContext.writeTensor(inputTensor2, Float32Array.from([10]));

  let readTensorPromises = [];

  mlContext.dispatch(
      mlGraph, {'input': inputTensor1}, {'output': outputTensor});

  // Don't await tensor readback before dispatching again.
  readTensorPromises.push(mlContext.readTensor(outputTensor));

  mlContext.dispatch(
      mlGraph, {'input': inputTensor2}, {'output': outputTensor});

  readTensorPromises.push(mlContext.readTensor(outputTensor));

  const actualOutputs =
      await Promise.all(readTensorPromises.map(async (promise) => {
        const output = await promise;
        return new Float32Array(output)[0];
      }));

  assert_array_equals(actualOutputs, [2, 20]);
}, 'dispatch queues behind readTensor');

promise_test(async () => {
  const operandDescriptor = {
    dataType: 'float32',
    shape: [1],
    readable: true,
    writable: true,
  };
  const mlGraph = await buildMulGraph(mlContext, operandDescriptor, 3);

  // write/dispatch/read, write/dispatch/read, ...
  const testInputs = [1, 2, 3, 4];
  const actualOutputs = await Promise.all(testInputs.map(async (input) => {
    const [inputTensor, outputTensor] = await Promise.all([
      mlContext.createTensor(operandDescriptor),
      mlContext.createTensor(operandDescriptor)
    ]);

    mlContext.writeTensor(inputTensor, Float32Array.from([input]));

    mlContext.dispatch(
        mlGraph, {'input': inputTensor}, {'output': outputTensor});

    const output = await mlContext.readTensor(outputTensor);
    return new Float32Array(output)[0];
  }));

  assert_array_equals(actualOutputs, [3, 6, 9, 12]);
}, 'same graph: write/dispatch/read, write/dispatch/read, ...');

promise_test(async () => {
  const operandDescriptor = {
    dataType: 'float32',
    shape: [1],
    readable: true,
    writable: true,
  };
  const mlGraph = await buildMulGraph(mlContext, operandDescriptor, 10);

  // write/write...
  const testInputs = [1, 2, 3, 4];
  const inputAndOutputTensors =
      await Promise.all(testInputs.map(async (testInput) => {
        const [inputTensor, outputTensor] = await Promise.all([
          mlContext.createTensor(operandDescriptor),
          mlContext.createTensor(operandDescriptor)
        ]);

        mlContext.writeTensor(inputTensor, Float32Array.from([testInput]));
        return [inputTensor, outputTensor];
      }));

  // dispatch/read, dispatch/read, ...
  let readTensorPromises = [];
  for (let i = 0; i < testInputs.length; i++) {
    mlContext.dispatch(
        mlGraph, {'input': inputAndOutputTensors[i][0]},
        {'output': inputAndOutputTensors[i][1]});
    readTensorPromises.push(mlContext.readTensor(inputAndOutputTensors[i][1]));
  };

  const actualOutputs =
      await Promise.all(readTensorPromises.map(async (promise) => {
        const output = await promise;
        return new Float32Array(output)[0];
      }));

  assert_array_equals(actualOutputs, [10, 20, 30, 40]);
}, 'same graph: write/write..., dispatch/read, dispatch/read, ...');

promise_test(async () => {
  const operandDescriptor = {
    dataType: 'float32',
    shape: [1],
    readable: true,
    writable: true,
  };
  const mlGraph = await buildMulGraph(mlContext, operandDescriptor, 9);

  // write/write...
  const testInputs = [1, 2, 3, 4];
  const inputAndOutputTensors =
      await Promise.all(testInputs.map(async (testInput) => {
        const [inputTensor, outputTensor] = await Promise.all([
          mlContext.createTensor(operandDescriptor),
          mlContext.createTensor(operandDescriptor)
        ]);

        mlContext.writeTensor(inputTensor, Float32Array.from([testInput]));
        return [inputTensor, outputTensor];
      }));

  // dispatch/dispatch...
  for (let i = 0; i < testInputs.length; i++) {
    mlContext.dispatch(
        mlGraph, {'input': inputAndOutputTensors[i][0]},
        {'output': inputAndOutputTensors[i][1]});
  }

  // read/read...
  const actualOutputs = await Promise.all(
      inputAndOutputTensors.map(async (inputAndOutputTensor) => {
        const output = await mlContext.readTensor(inputAndOutputTensor[1]);
        return new Float32Array(output)[0];
      }));

  assert_array_equals(actualOutputs, [9, 18, 27, 36]);
}, 'same graph: write/write..., dispatch/dispatch..., read/read...');

promise_test(async () => {
  const operandDescriptor = {
    dataType: 'float32',
    shape: [1],
    readable: true,
    writable: true,
  };
  const mlGraph = await buildMulGraph(mlContext, operandDescriptor, 2);

  const tensors = await Promise.all([
    mlContext.createTensor(operandDescriptor),
    mlContext.createTensor(operandDescriptor),
    mlContext.createTensor(operandDescriptor),
    mlContext.createTensor(operandDescriptor),
    mlContext.createTensor(operandDescriptor)
  ]);

  mlContext.writeTensor(tensors[0], Float32Array.from([1]));

  // dispatch/dispatch...
  for (let i = 0; i < tensors.length - 1; i++) {
    mlContext.dispatch(
        mlGraph, {'input': tensors[i]}, {'output': tensors[i + 1]});
  }

  // read/read...
  const actualOutputs = await Promise.all(tensors.map(async (tensor) => {
    const output = await mlContext.readTensor(tensor);
    return new Float32Array(output)[0];
  }));

  assert_array_equals(actualOutputs, [1, 2, 4, 8, 16]);
}, 'same graph serial inputs: dispatch/dispatch..., read/read...');

promise_test(async () => {
  const operandDescriptor = {
    dataType: 'float32',
    shape: [1],
    readable: true,
    writable: true,
  };

  // write/write...
  const testInputs = [1, 2, 3, 4];
  const graphsAndTensors =
      await Promise.all(testInputs.map(async (testInput) => {
        const [graph, inputTensor, outputTensor] = await Promise.all([
          buildMulGraph(mlContext, operandDescriptor, testInput),
          mlContext.createTensor(operandDescriptor),
          mlContext.createTensor(operandDescriptor)
        ]);

        mlContext.writeTensor(inputTensor, Float32Array.from([testInput]));
        return [graph, inputTensor, outputTensor];
      }));

  // dispatch/read, dispatch/read, ...
  let readTensorPromises = [];
  for (let i = 0; i < graphsAndTensors.length; i++) {
    mlContext.dispatch(
        graphsAndTensors[i][0], {'input': graphsAndTensors[i][1]},
        {'output': graphsAndTensors[i][2]});
    readTensorPromises.push(mlContext.readTensor(graphsAndTensors[i][2]));
  };

  const actualOutputs =
      await Promise.all(readTensorPromises.map(async (promise) => {
        const output = await promise;
        return new Float32Array(output)[0];
      }));

  assert_array_equals(actualOutputs, [1, 4, 9, 16]);
}, 'different graphs: write/write..., dispatch/read, dispatch/read, ...');

promise_test(async () => {
  const operandDescriptor = {
    dataType: 'float32',
    shape: [1],
    readable: true,
    writable: true,
  };

  // write/write...
  const testInputs = [1, 2, 3, 4];
  const graphsAndTensors =
      await Promise.all(testInputs.map(async (testInput) => {
        const [graph, inputTensor, outputTensor] = await Promise.all([
          buildMulGraph(mlContext, operandDescriptor, testInput * 2),
          mlContext.createTensor(operandDescriptor),
          mlContext.createTensor(operandDescriptor)
        ]);

        mlContext.writeTensor(inputTensor, Float32Array.from([testInput]));
        return [graph, inputTensor, outputTensor];
      }));

  // dispatch/dispatch...
  for (let i = 0; i < graphsAndTensors.length; i++) {
    mlContext.dispatch(
        graphsAndTensors[i][0], {'input': graphsAndTensors[i][1]},
        {'output': graphsAndTensors[i][2]});
  };

  // read/read...
  const actualOutputs =
      await Promise.all(graphsAndTensors.map(async (graphAndTensors) => {
        const output = await mlContext.readTensor(graphAndTensors[2]);
        return new Float32Array(output)[0];
      }));

  assert_array_equals(actualOutputs, [2, 8, 18, 32]);
}, 'different graphs: write/write..., dispatch/dispatch..., read/read...');

promise_test(async () => {
  const operandDescriptor = {
    dataType: 'float32',
    shape: [1],
    readable: true,
    writable: true,
  };

  const graphs = await Promise.all([3, 2].map(async (multiplier) => {
    return buildMulGraph(mlContext, operandDescriptor, multiplier);
  }));

  const tensors = await Promise.all([
    mlContext.createTensor(operandDescriptor),
    mlContext.createTensor(operandDescriptor),
    mlContext.createTensor(operandDescriptor),
    mlContext.createTensor(operandDescriptor),
    mlContext.createTensor(operandDescriptor)
  ]);

  mlContext.writeTensor(tensors[0], Float32Array.from([1]));

  // dispatch/dispatch...
  for (let i = 0; i < tensors.length - 1; i++) {
    mlContext.dispatch(
        graphs[i % 2], {'input': tensors[i]}, {'output': tensors[i + 1]});
  }

  // read/read...
  const actualOutputs = await Promise.all(tensors.map(async (tensor) => {
    const output = await mlContext.readTensor(tensor);
    return new Float32Array(output)[0];
  }));

  assert_array_equals(actualOutputs, [1, 3, 6, 18, 36]);
}, 'different graphs serial inputs: dispatch/dispatch..., read/read...');

promise_test(async () => {
  const operandDescriptor = {
    dataType: 'float32',
    shape: [1],
    readable: true,
    writable: true,
  };

  const graphs = await Promise.all([2, 3].map(async (multiplier) => {
    return buildMulGraph(mlContext, operandDescriptor, multiplier);
  }));

  const tensors = await Promise.all([
    mlContext.createTensor(operandDescriptor),
    mlContext.createTensor(operandDescriptor)
  ]);

  // Write to the tensor which will be initially used as an input.
  mlContext.writeTensor(tensors[0], Float32Array.from([1]));

  // Double the value in one tensor, sticking the result in the other tensor.
  //
  // tensors[0]  tensors[1]
  //     1
  //        >---->  2
  //     6  <----<
  //        >---->  12
  //     36 <----<
  //        >---->  72
  //    216 <----<

  // dispatch/dispatch...
  for (let i = 0; i < 6; i++) {
    mlContext.dispatch(
        graphs[i % 2], {'input': tensors[i % 2]},
        {'output': tensors[(i + 1) % 2]});
  };

  // read/read...
  const actualOutputs = await Promise.all(tensors.map(async (tensor) => {
    const output = await mlContext.readTensor(tensor);
    return new Float32Array(output)[0];
  }));

  assert_array_equals(actualOutputs, [216, 72]);
}, 'different graphs using the same tensors');
