web-dev-qa-db-ja.com

JavaScriptで非同期関数の実行時間を計算する方法は?

非同期関数(async/await)がJavaScriptでかかる時間を計算したいと思います。

できること:

_const asyncFunc = async function () {};

const before = Date.now();
asyncFunc().then(() => {
  const after = Date.now();
  console.log(after - before);
});
_

ただし、promiseコールバックは新しいマイクロタスクで実行されるため、これは機能しません。つまりasyncFunc()の終わりからthen(() => {})の始まりまでの間に、すでにキューに入れられているマイクロタスクが最初に実行され、それらの実行時間が考慮されます。

例えば。:

_const asyncFunc = async function () {};

const slowSyncFunc = function () {
  for (let i = 1; i < 10 ** 9; i++) {}
};

process.nextTick(slowSyncFunc);

const before = Date.now();
asyncFunc().then(() => {
  const after = Date.now();
  console.log(after - before);
});
_

これは私のマシンに_1739_を出力します。つまり、slowSyncFunc()が完了するのを待つため、ほぼ2秒です。これは間違っています。

asyncFuncの本体を変更したくないことに注意してください。それぞれを変更する負担なしに、多くの非同期関数をインストルメントする必要があるからです。それ以外の場合は、asyncFuncの最初と最後にDate.now()ステートメントを追加するだけで済みます。

また、問題はパフォーマンスカウンターの取得方法ではないことにも注意してください。 Date.now()console.time()process.hrtime()(Node.jsのみ)またはperformance(ブラウザーのみ)を使用しても、この問題の原因は変わりません。問題は、promiseコールバックが新しいマイクロタスクで実行されるという事実に関するものです。元の例にsetTimeoutや_process.nextTick_のようなステートメントを追加すると、問題が修正されます。

9
ehmicky

すでにキューに入れられているマイクロタスクが最初に実行され、それらの実行時間が考慮されます。

はい、それを回避する方法はありません。他のタスクを測定に貢献させたくない場合は、キューに入れないでください。それが唯一の解決策です。

これは約束の問題ではありません(またはasync functions)または特にマイクロタスクキューの問題です。これは、タスクキューでコールバックを実行するall非同期のものによって共有される問題です。

4
Bergi

私たちが抱えている問題

_process.nextTick(() => {/* hang 100ms */})
const asyncFunc = async () => {/* hang 10ms */}
const t0 = /* timestamp */
asyncFunc().then(() => {
  const t1 = /* timestamp */
  const timeUsed = t1 - t0 /* 110ms because of nextTick */
  /* WANTED: timeUsed = 10ms */
})
_

解決策(アイデア)

_const AH = require('async_hooks')
const hook = /* AH.createHook for
   1. Find async scopes that asycnFunc involves ... SCOPES
      (by handling 'init' hook)
   2. Record time spending on these SCOPES ... RECORDS 
      (by handling 'before' & 'after' hook) */
hook.enable()
asyncFunc().then(() => {
  hook.disable()
  const timeUsed = /* process RECORDS */
})
_

しかし、これは最初の同期操作をキャプチャしません。つまり、以下のようにasyncFuncを想定すると、_$1$_はSCOPESに追加されず(sync opであるため、async_hooksは新しい非同期スコープを初期化しません)、RECORDSに時間レコードを追加しません。

_hook.enable()
/* A */
(async function asyncFunc () { /* B */
  /* hang 10ms; usually for init contants etc ... $1$ */ 
  /* from async_hooks POV, scope A === scope B) */
  await /* async scope */
}).then(..)
_

これらの同期操作を記録するための簡単な解決策は、setTimeoutにラップすることにより、新しいascynスコープで実行するように強制することです。この余分なものは実行に時間がかかります。値が非常に小さいので無視してください

_hook.enable()
/* force async_hook to 'init' new async scope */
setTimeout(() => { 
   const t0 = /* timestamp */
   asyncFunc()
    .then(()=>{hook.disable()})
    .then(()=>{
      const timeUsed = /* process RECORDS */
    })
   const t1 = /* timestamp */
   t1 - t0 /* ~0; note that 2 `then` callbacks will not run for now */ 
}, 1)
_

解決策は、 '非同期関数が関与するsync opsに費やされた時間を測定することであることに注意してください'、async ops例:タイムアウトアイドルはカウントされません。

_async () => {
  /* hang 10ms; count*/
  await new Promise(resolve => {
    setTimeout(() => {
      /* hang 10ms; count */
      resolve()
    }, 800/* NOT count*/)
  }
  /* hang 10ms; count*/
}
// measurement takes 800ms to run
// timeUsed for asynFunc is 30ms
_

最後に、async_hooksはスケジューリングの詳細を提供するため、同期操作と非同期操作の両方を含む方法で非同期関数を測定できる可能性があると思います(たとえば、800msを決定できます)。 setTimeout(f, ms)、async_hooksは「タイムアウト」タイプの非同期スコープを初期化します。スケジューリングの詳細msは、_resource._idleTimeout_のinit(,,,resource)フックにあります。


デモ(nodejs v8.4.0でテスト済み)

_// measure.js
const { writeSync } = require('fs')
const { createHook } = require('async_hooks')

class Stack {
  constructor() {
    this._array = []
  }
  Push(x) { return this._array.Push(x) }
  peek() { return this._array[this._array.length - 1] }
  pop() { return this._array.pop() }
  get is_not_empty() { return this._array.length > 0 }
}

class Timer {
  constructor() {
    this._records = new Map/* of {start:number, end:number} */
  }
  starts(scope) {
    const detail =
      this._records.set(scope, {
        start: this.timestamp(),
        end: -1,
      })
  }
  ends(scope) {
    this._records.get(scope).end = this.timestamp()
  }
  timestamp() {
    return Date.now()
  }
  timediff(t0, t1) {
    return Math.abs(t0 - t1)
  }
  report(scopes, detail) {
    let tSyncOnly = 0
    let tSyncAsync = 0
    for (const [scope, { start, end }] of this._records)
      if (scopes.has(scope))
        if (~end) {
          tSyncOnly += end - start
          tSyncAsync += end - start
          const { type, offset } = detail.get(scope)
          if (type === "Timeout")
            tSyncAsync += offset
          writeSync(1, `async scope ${scope} \t... ${end - start}ms \n`)
        }
    return { tSyncOnly, tSyncAsync }
  }
}

async function measure(asyncFn) {
  const stack = new Stack
  const scopes = new Set
  const timer = new Timer
  const detail = new Map
  const hook = createHook({
    init(scope, type, parent, resource) {
      if (type === 'TIMERWRAP') return
      scopes.add(scope)
      detail.set(scope, {
        type: type,
        offset: type === 'Timeout' ? resource._idleTimeout : 0
      })
    },
    before(scope) {
      if (stack.is_not_empty) timer.ends(stack.peek())
      stack.Push(scope)
      timer.starts(scope)
    },
    after() {
      timer.ends(stack.pop())
    }
  })

  // Force to create a new async scope by wrapping asyncFn in setTimeout,
  // st sync part of asyncFn() is a async op from async_hooks POV.
  // The extra async scope also take time to run which should not be count
  return await new Promise(r => {
    hook.enable()
    setTimeout(() => {
      asyncFn()
        .then(() => hook.disable())
        .then(() => r(timer.report(scopes, detail)))
        .catch(console.error)
    }, 1)
  })
}
_

テスト

_// arrange
const hang = (ms) => {
  const t0 = Date.now()
  while (Date.now() - t0 < ms) { }
}
const asyncFunc = async () => {
  hang(16)                           // 16
  try {
    await new Promise(r => {
      hang(16)                       // 16
      setTimeout(() => {
        hang(16)                     // 16
        r()
      }, 100)                        // 100
    })
    hang(16)                         // 16
  } catch (e) { }
  hang(16)                           // 16
}
// act
process.nextTick(() => hang(100))    // 100
measure(asyncFunc).then(report => {
  // inspect
  const { tSyncOnly, tSyncAsync } = report
  console.log(`
  ∑ Sync Ops       = ${tSyncOnly}ms \t (expected=${16 * 5})
  ∑ Sync&Async Ops = ${tSyncAsync}ms \t (expected=${16 * 5 + 100})
  `)
}).catch(e => {
  console.error(e)
})
_

結果

_async scope 3   ... 38ms
async scope 14  ... 16ms
async scope 24  ... 0ms
async scope 17  ... 32ms

  ∑ Sync Ops       = 86ms       (expected=80)
  ∑ Sync&Async Ops = 187ms      (expected=180)
_
2
user943702

Perfrmance.now()APIの使用を検討してください

_var time_0 = performance.now();
function();
var time_1 = performance.now();
console.log("Call to function took " + (time_1 - time_0) + " milliseconds.")
_

performance.now()は_console.time_の最低限のバージョンであるため、より正確なタイミングを提供します。

0
DivyaMaheswaran