【问题标题】:How to calculate the execution time of an async function in JavaScript?如何计算 JavaScript 中异步函数的执行时间?
【发布时间】:2017-08-18 08:50:08
【问题描述】:

我想计算一个异步函数 (async/await) 在 JavaScript 中占用了多长时间。

可以做到:

const asyncFunc = async function () {};

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

但是,这不起作用,因为 Promise 回调在新的微任务中运行。 IE。在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,即将近 2 秒,因为它等待 slowSyncFunc() 完成,这是错误的。

请注意,我不想修改 asyncFunc 的主体,因为我需要检测许多异步函数,而无需修改每个函数。否则我可以在asyncFunc 的开头和结尾添加一个Date.now() 语句。

还要注意,问题不在于如何检索性能计数器。使用Date.now()console.time()process.hrtime()(仅限Node.js)或performance(仅限浏览器)不会改变这个问题的根源。问题在于承诺回调是在一个新的微任务中运行的。如果您在原始示例中添加 setTimeoutprocess.nextTick 之类的语句,则说明您正在修改问题。

【问题讨论】:

  • 这个问题的答案并不能解决我的问题。最佳答案基本上是我第一个例子的另一种表述。它没有解决事件循环的问题,即 Promise 总是在一个新的微任务中运行,而其他待处理的微任务将首先运行。

标签: javascript asynchronous async-await benchmarking


【解决方案1】:

我们遇到的问题

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(因为它是同步操作,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(..)

要记录这些同步操作,一个简单的解决方案是强制它们在新的 ascyn 范围内运行,方法是包装到 setTimeout 中。这个额外的东西确实需要时间来运行,忽略它,因为它的值很小

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

最后,我认为有可能以包括同步和异步操作的方式测量异步功能(例如可以确定 800 毫秒),因为 async_hooks 确实提供了调度的详细信息,例如setTimeout(f, ms),async_hooks 将初始化一个“Timeout”类型的异步范围,调度细节,ms,可以在resource._idleTimeoutinit(,,,resource) hook 中找到


演示(在 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】:

    考虑使用 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 的基本版本,它提供更准确的计时。

    【讨论】:

    • 这并不能解决这个问题。尝试通过将Date.now() 替换为performance.now() 来运行我的第二个示例,您会发现同样的问题。问题是关于在新微任务中运行的承诺回调。无论您使用performance.now 还是其他方式,任何已排队的微任务都将首先运行。
    【解决方案3】:

    任何已经排队的微任务将首先被触发,并且它们的执行时间将被考虑在内。

    是的,没有办法解决这个问题。如果您不想让其他任务对您的测量做出贡献,请不要排队。这是唯一的解决方案。

    这不是承诺(或async functions)或微任务队列的问题,而是所有在任务队列上运行回调的异步事物共享的问题。

    【讨论】:

    • 问题是这种情况发生在 Web 服务器上,从各个方向触发了许多微任务,因此当 asyncFunc 停止执行时,我无法强制任务队列为空。
    • 我看不出有什么问题。您是否想在真实环境中进行测量。它是哪一个?顺便说一句,如果您确保没有任务缓慢且阻塞,那么无论如何这应该不会有太大的不同。
    • 我想在真实环境(生产环境)中进行测量。统计信息被发送到性能监控仪表板。我的 Web 服务器触发了 70 多个异步中间件函数的管道,一个接一个地触发,有时针对同一个请求并行触发。许多微任务和宏任务并行运行。我想知道每个异步中间件函数需要多长时间,而不必修改它们。
    • 此外,大多数微任务在您当前的中间件在后台处理异步内容时运行。所以任务队列大部分时间应该是空的,并且不会严重影响您的测量 - 如果有的话,那么最多是一个常数因子。还是您实际上只想测量同步任务部分?
    • 恐怕那是不可能的。与 nextTicksetTimeout 不同,它们接受您可以检测的回调,没有办法挂钩到本机 async functions(可能除了一些调试 API)。我想您最好的选择是使用转译器在每个 await 运算符周围注入停止和启动代码。
    【解决方案4】:

    您可以使用console.time('nameit')console.timeEnd('nameit') 查看下面的示例。

    console.time('init')
    
    const asyncFunc = async function () {
    };
    
    const slowSyncFunc = function () {
      for (let i = 1; i < 10 ** 9; i++) {}
    };
    // let's slow down a bit.
    slowSyncFunc()
    console.time('async')
    asyncFunc().then((data) => {
      console.timeEnd('async')  
    });
    
    console.timeEnd('init')

    【讨论】:

    • 这并不能解决这个问题。您在asyncFunc 中添加了setTimeout 语句。因此,它将在一个新的宏任务中运行,即它将在aslowSyncFunc 之后结束。尝试删除setTimeout,问题又出现了。
    • 再检查一遍? init: 1474.230ms - async: 3.345ms
    • 您更改了示例,但仍然与我原来的示例不同。 slowSyncFunc 不再在带有 process.nextTick 的新微任务中运行。 IE。它在asyncFunc 甚至开始之前就结束了,所以现在可以使用了。尝试添加process.nextTick,问题又出现了。
    猜你喜欢
    • 2021-08-10
    • 2017-11-30
    • 1970-01-01
    • 2018-11-03
    • 1970-01-01
    • 2021-05-31
    • 2021-07-15
    • 2015-07-19
    • 2020-07-16
    相关资源
    最近更新 更多