🏡 index : cold-air-networks/mirror-jetstream.git

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

// Excerpt from `wasm_gc_benchmarks/tools/run_wasm.js` to add own task queue
// implementation, since `setTimeout` and `queueMicrotask` are not always
// available in shells.
// TODO: Now (2025-08-14) that all shells have `setTimeout` available, can we
// remove this? Talk to Dart2wasm folks.
function addTaskQueue(self) {
  "use strict";

    // Task queue as cyclic list queue.
    var taskQueue = new Array(8);  // Length is power of 2.
    var head = 0;
    var tail = 0;
    var mask = taskQueue.length - 1;

    function addTask(elem) {
      taskQueue[head] = elem;
      head = (head + 1) & mask;
      if (head == tail) _growTaskQueue();
    }

    function removeTask() {
      if (head == tail) return;
      var result = taskQueue[tail];
      taskQueue[tail] = undefined;
      tail = (tail + 1) & mask;
      return result;
    }

    function _growTaskQueue() {
      // head == tail.
      var length = taskQueue.length;
      var split = head;
      taskQueue.length = length * 2;
      if (split * 2 < length) {  // split < length / 2
        for (var i = 0; i < split; i++) {
          taskQueue[length + i] = taskQueue[i];
          taskQueue[i] = undefined;
        }
        head += length;
      } else {
        for (var i = split; i < length; i++) {
          taskQueue[length + i] = taskQueue[i];
          taskQueue[i] = undefined;
        }
        tail += length;
      }
      mask = taskQueue.length - 1;
    }

    // Mapping from timer id to timer function.
    // The timer id is written on the function as .$timerId.
    // That field is cleared when the timer is cancelled, but it is not returned
    // from the queue until its time comes.
    var timerIds = {};
    var timerIdCounter = 1;  // Counter used to assign ids.

    // Zero-timer queue as simple array queue using push/shift.
    var zeroTimerQueue = [];

    function addTimer(f, ms) {
      ms = Math.max(0, ms);
      var id = timerIdCounter++;
      // A callback can be scheduled at most once.
      console.assert(f.$timerId === undefined);
      f.$timerId = id;
      timerIds[id] = f;
      if (ms == 0 && !isNextTimerDue()) {
        zeroTimerQueue.push(f);
      } else {
        addDelayedTimer(f, ms);
      }
      return id;
    }

    function nextZeroTimer() {
      while (zeroTimerQueue.length > 0) {
        var action = zeroTimerQueue.shift();
        if (action.$timerId !== undefined) return action;
      }
    }

    function nextEvent() {
      var action = removeTask();
      if (action) {
        return action;
      }
      do {
        action = nextZeroTimer();
        if (action) break;
        var nextList = nextDelayedTimerQueue();
        if (!nextList) {
          return;
        }
        var newTime = nextList.shift();
        advanceTimeTo(newTime);
        zeroTimerQueue = nextList;
      } while (true)
      var id = action.$timerId;
      clearTimerId(action, id);
      return action;
    }

    // Mocking time.
    var timeOffset = 0;
    var now = function() {
      // Install the mock Date object only once.
      // Following calls to "now" will just use the new (mocked) Date.now
      // method directly.
      installMockDate();
      now = Date.now;
      return Date.now();
    };
    var originalDate = Date;
    var originalNow = originalDate.now;

    function advanceTimeTo(time) {
      var now = originalNow();
      if (timeOffset < time - now) {
        timeOffset = time - now;
      }
    }

    function installMockDate() {
      var NewDate = function Date(Y, M, D, h, m, s, ms) {
        if (this instanceof Date) {
          // Assume a construct call.
          switch (arguments.length) {
            case 0:  return new originalDate(originalNow() + timeOffset);
            case 1:  return new originalDate(Y);
            case 2:  return new originalDate(Y, M);
            case 3:  return new originalDate(Y, M, D);
            case 4:  return new originalDate(Y, M, D, h);
            case 5:  return new originalDate(Y, M, D, h, m);
            case 6:  return new originalDate(Y, M, D, h, m, s);
            default: return new originalDate(Y, M, D, h, m, s, ms);
          }
        }
        return new originalDate(originalNow() + timeOffset).toString();
      };
      NewDate.UTC = originalDate.UTC;
      NewDate.parse = originalDate.parse;
      NewDate.now = function now() { return originalNow() + timeOffset; };
      NewDate.prototype = originalDate.prototype;
      originalDate.prototype.constructor = NewDate;
      Date = NewDate;
    }

    // Heap priority queue with key index.
    // Each entry is list of [timeout, callback1 ... callbackn].
    var timerHeap = [];
    var timerIndex = {};

    function addDelayedTimer(f, ms) {
      var timeout = now() + ms;
      var timerList = timerIndex[timeout];
      if (timerList == null) {
        timerList = [timeout, f];
        timerIndex[timeout] = timerList;
        var index = timerHeap.length;
        timerHeap.length += 1;
        bubbleUp(index, timeout, timerList);
      } else {
        timerList.push(f);
      }
    }

    function isNextTimerDue() {
      if (timerHeap.length == 0) return false;
      var head = timerHeap[0];
      return head[0] < originalNow() + timeOffset;
    }

    function nextDelayedTimerQueue() {
      if (timerHeap.length == 0) return null;
      var result = timerHeap[0];
      var last = timerHeap.pop();
      if (timerHeap.length > 0) {
        bubbleDown(0, last[0], last);
      }
      return result;
    }

    function bubbleUp(index, key, value) {
      while (index != 0) {
        var parentIndex = (index - 1) >> 1;
        var parent = timerHeap[parentIndex];
        var parentKey = parent[0];
        if (key > parentKey) break;
        timerHeap[index] = parent;
        index = parentIndex;
      }
      timerHeap[index] = value;
    }

    function bubbleDown(index, key, value) {
      while (true) {
        var leftChildIndex = index * 2 + 1;
        if (leftChildIndex >= timerHeap.length) break;
        var minChildIndex = leftChildIndex;
        var minChild = timerHeap[leftChildIndex];
        var minChildKey = minChild[0];
        var rightChildIndex = leftChildIndex + 1;
        if (rightChildIndex < timerHeap.length) {
          var rightChild = timerHeap[rightChildIndex];
          var rightKey = rightChild[0];
          if (rightKey < minChildKey) {
            minChildIndex = rightChildIndex;
            minChild = rightChild;
            minChildKey = rightKey;
          }
        }
        if (minChildKey > key) break;
        timerHeap[index] = minChild;
        index = minChildIndex;
      }
      timerHeap[index] = value;
    }

    function cancelTimer(id) {
      var f = timerIds[id];
      if (f == null) return;
      clearTimerId(f, id);
    }

    function clearTimerId(f, id) {
      f.$timerId = undefined;
      delete timerIds[id];
    }

    async function eventLoop(action) {
      while (action) {
        try {
          await action();
        } catch (e) {
          if (typeof onerror == "function") {
            onerror(e, null, -1);
          } else {
            throw e;
          }
        }
        action = nextEvent();
      }
    }

    self.setTimeout = addTimer;
    self.clearTimeout = cancelTimer;
    self.queueMicrotask = addTask;
    self.eventLoop = eventLoop;
}

function dartPrint(...args) { print(args); }
addTaskQueue(globalThis);
globalThis.window ??= globalThis;

class Benchmark {
  dart2wasmJsModule;
  compiledApp;

  async init() {
    // The generated JavaScript code from dart2wasm is an ES module, which we
    // can only load with a dynamic import (since this file is not a module.)

    Module.wasmBinary = await JetStream.getBinary(JetStream.preload.wasmBinary);
    this.dart2wasmJsModule = await JetStream.dynamicImport(JetStream.preload.jsModule);
  }

  async runIteration() {
    // Compile once in the first iteration.
    if (!this.compiledApp) {
      this.compiledApp = await this.dart2wasmJsModule.compile(Module.wasmBinary);
    }

    // Instantiate each iteration, since we can only `invokeMain()` with a
    // freshly instantiated module.
    const additionalImports = {};
    const instantiatedApp = await this.compiledApp.instantiate(additionalImports);

    const startTimeSinceEpochSeconds = new Date().getTime() / 1000;
    // Reduce workload size for a single iteration.
    // The default is 1000 frames, but that takes too long (>2s per iteration).
    const framesToDraw = 100;
    const initialFramesToSkip = 0;
    const dartArgs = [
      startTimeSinceEpochSeconds.toString(),
      framesToDraw.toString(),
      initialFramesToSkip.toString()
    ];

    await eventLoop(async () => {
      await instantiatedApp.invokeMain(...dartArgs)
    });
  }
}