All Articles

V8 Blog | Faster async functions and promises 2018-11-12

1. 原文

Faster async functions and promises

上一篇排序偷懒比较厉害,这篇相对来说比较重要,就重点全文翻译了。原文我仍旧留着,以作对照用。

2. 摘要翻译

Asynchronous processing in JavaScript traditionally had a reputation for not being particularly fast. To make matters worse, debugging live JavaScript applications — in particular Node.js servers — is no easy task, especially when it comes to async programming. Luckily the times, they are a-changin’. This article explores how we optimized async functions and promises in V8 (and to some extent in other JavaScript engines as well), and describes how we improved the debugging experience for async code.

JavaScript的异步处理不够快已经是名声在外了。更糟糕的是在JavaScript中对应用程序进行debug - 特别是Node.js服务器 - 很困难,特别是异步编程的情况。幸运的是,事情正在转变。这篇文章会探索我们在V8中是如何优化异步函数和promise的,并描述我们是如何提升异步代码的debugging体验的。

A new approach to async programming

From callbacks to promises to async functions

Before promises were part of the JavaScript language, callback-based APIs were commonly used for asynchronous code, especially in Node.js. Here’s an example:

在promise出现之前,基于callback的APIs是异步代码的实现选择,特别是在Node.js中。这里有一个例子:

function handler(done) {
  validateParams((error) => {
    if (error) return done(error);
    dbQuery((error, dbResults) => {
      if (error) return done(error);
      serviceCall(dbResults, (error, serviceResults) => {
        console.log(result);
        done(error, serviceResults);
      });
    });
  });
}

The specific pattern of using deeply-nested callbacks in this manner is commonly referred to as “callback hell”, because it makes the code less readable and hard to maintain.

这种深度嵌套的callback写法被称为”回调地狱”,因为这样的代码使得可读性和维护性都非常差。

Luckily, now that promises are part of the JavaScript language, the same code could be written in a more elegant and maintainable manner:

幸运的是,现在promise已经来了,同样的逻辑,代码可以写得更优雅更可维护:

function handler() {
  return validateParams()
    .then(dbQuery)
    .then(serviceCall)
    .then(result => {
      console.log(result);
      return result;
    });
}

Even more recently, JavaScript gained support for async functions. The above asynchronous code can now be written in a way that looks very similar to synchronous code:

最近,JavaScript得到了async函数的支持。上述的异步操作代码现在可以写成类似同步的代码:

async function handler() {
  await validateParams();
  const dbResults = await dbQuery();
  const results = await serviceCall(dbResults);
  console.log(results);
  return results;
}

With async functions, the code becomes more succinct, and the control and data flow are a lot easier to follow, despite the fact that the execution is still asynchronous. (Note that the JavaScript execution still happens in a single thread, meaning async functions don’t end up creating physical threads themselves.)

有了async函数的支持,代码变得更简洁,代码控制以及数据流更容易追踪,尽管事实上这些代码运行时仍旧是异步执行的。(请注意,JavaScript代码的执行仍旧是在单线程中,意味着async函数并未给它们自己创建物理上的线程。)

From event listener callbacks to async iteration

Another asynchronous paradigm that’s especially common in Node.js is that of ReadableStreams. Here’s an example:

另一个在Node.ReadableStreams。这里有个例子:

const http = require('http');

http.createServer((req, res) => {
  let body = '';
  req.setEncoding('utf8');
  req.on('data', (chunk) => {
    body += chunk;
  });
  req.on('end', () => {
    res.write(body);
    res.end();
  });
}).listen(1337);

This code can be a little hard to follow: the incoming data is processed in chunks that are only accessible within callbacks, and the end-of-stream signaling happens inside a callback too. It’s easy to introduce bugs here when you don’t realize that the function terminates immediately and that the actual processing has to happen in the callbacks.

这段代码有点难以追踪:输入的数据仅仅只能在callback中访问到,并且end-of-stream信号也只能在另一个回调函数中得到通知。如果你并没有理解函数在运行时立马结束了,而真正的处理是放生在回调函数中的话,就很容易产生bug。

Fortunately, a cool new ES2018 feature called async iteration can simplify this code:

幸运的是,ES2018中引入了一个很cool的功能,称为 async iteration,能简化这段代码:

const http = require('http');

http.createServer(async (req, res) => {
  try {
    let body = '';
    req.setEncoding('utf8');
    for await (const chunk of req) {
      body += chunk;
    }
    res.write(body);
    res.end();
  } catch {
    res.statusCode = 500;
    res.end();
  }
}).listen(1337);

Instead of putting the logic that deals with the actual request processing into two different callbacks — the 'data' and the 'end' callback — we can now put everything into a single async function instead, and use the new for await…of loop to iterate over the chunks asynchronously. We also added a try-catch block to avoid the unhandledRejection problem.

不再将请求处理的逻辑放在两个不同的回调函数里 - 'data' 以及 'end' 回调 - 我们现在可以将所有逻辑放在一个async函数里处理即可,并且使用新的for await…of循环来异步迭代chunks数据。我们还添加了一个try-catch代码块来防止unhandledRejection问题。

You can already use these new features in production today! Async functions are fully supported starting with Node.js 8 (V8 v6.2 / Chrome 62), and async iterators and generators are fully supported starting with Node.js 10 (V8 v6.8 / Chrome 68)!

你已经可以在生产环境中使用这些新功能了!async函数从Node.js 8(V8 v6.2 / Chrome 62)就得到全面支持了,而async迭代以及generators则从Node.js 10(V8 v6.8 / Chrome 68)开始得到全面支持

Async performance improvements

We’ve managed to improve the performance of asynchronous code significantly between V8 v5.5 (Chrome 55 & Node.js 7) and V8 v6.8 (Chrome 68 & Node.js 10). We reached a level of performance where developers can safely use these new programming paradigms without having to worry about speed.

我们已经在V8 v5.5(Chrome 55 & Node.js 7)以及V8 v6.8(Chrome 68 & Node.js 10)版本之间显著提升了异步代码的性能。现在的性能已经可以让开发者放心安全使用这些新的语法功能,而不用担心性能问题。

The above chart shows the doxbee benchmark, which measures performance of promise-heavy code. Note that the charts visualize execution time, meaning lower is better.

上图显示了doxbee benchmark,这个测试是用来衡量promise重度使用代码的性能。请注意,图表可视化了执行时长,这意味着越低越好。

The results on the parallel benchmark, which specifically stresses the performance of Promise.all(), are even more exciting:

下图显示了parallel benchmark,这个测试是用来压测Promise.all()的性能,很有趣:

We’ve managed to improve Promise.all performance by a factor of .

我们已经将Promise.all的性能提升了8x。

However, the above benchmarks are synthetic micro-benchmarks. The V8 team is more interested in how our optimizations affect real-world performance of actual user code.

然而,上述benchmarks是人为设定的微型benchmarks。V8团队对我们的优化是如何影响真实场景用户代码性能更感兴趣。

The above chart visualizes the performance of some popular HTTP middleware frameworks that make heavy use of promises and async functions. Note that this graph shows the number of requests/second, so unlike the previous charts, higher is better. The performance of these frameworks improved significantly between Node.js 7 (V8 v5.5) and Node.js 10 (V8 v6.8).

上图显示了一些重度使用promise和async函数的流行HTTP中间件框架的性能表现。请注意这幅图表显示了 requests / second 的数量,所以不像之前的图表,现在是越高越好。这些框架的性能在Node.js 7(V8 v5.5)和Node.js 10(V8 v6.8)之间提升明显。

These performance improvements are the result of three key achievements:

这些性能提升来自于三项关键成就的结果:

  • TurboFan, the new optimizing compiler 🎉

  • Orinoco, the new garbage collector 🚛

  • a Node.js 8 bug causing await to skip microticks 🐛

  • TurboFan,最新的优化编译器

  • Orinoco,新的GC垃圾回收器

  • 一个导致await跳过microticks的Node.js 8 bug

When we launched TurboFan in Node.js 8, that gave a huge performance boost across the board.

当我们在Node.js 8上线TurboFan的时候,得到了一个超级巨大的全面性能提升。

We’ve also been working on a new garbage collector, called Orinoco, which moves garbage collection work off the main thread, and thus improves request processing significantly as well.

我们也正在制作一个新的GC垃圾回收器,被称为Orinoco,它将垃圾回收工作从主线程中剥离出来,也显著提升了请求处理的量。

And last but not least, there was a handy bug in Node.js 8 that caused await to skip microticks in some cases, resulting in better performance. The bug started out as an unintended spec violation, but it later gave us the idea for an optimization. Let’s start by explaining the buggy behavior:

放在最后说,但不代表最不重要,在Node.js 8中有一个bug,导致了在某些情况下await会跳过microticks,导致了更好的性能表现。这个bug来源于一个非自觉的spec违反,但它也给了我们一些优化的灵感。让我们从解释这个bug行为开始:

const p = Promise.resolve();

(async () => {
  await p; console.log('after:await');
})();

p.then(() => console.log('tick:a'))
 .then(() => console.log('tick:b'));

The above program creates a fulfilled promise p, and awaits its result, but also chains two handlers onto it. In which order would you expect the console.log calls to execute?

上述程序创建了一个被满足的promisep,然后await它的结果,但仍旧链式附加了两个处理函数到这个promise上。你觉得最后console.log的调用顺序会如何?

Since p is fulfilled, you might expect it to print 'after:await' first and then the 'tick's. In fact, that’s the behavior you’d get in Node.js 8:

因为 p 已经被满足了,你可能觉得应该会先打印'after:await',然后才是'tick'。事实上,在Node.js 8中,确实是这个顺序:

Although this behavior seems intuitive, it’s not correct according to the specification. Node.js 10 implements the correct behavior, which is to first execute the chained handlers, and only afterwards continue with the async function.

虽然这个行为结果符合直觉,但其实按照spec来说,它是不正确的。Node.js 10实现了正确的行为,先执行被链式附加的两个处理函数,然后才会继续这个async函数。

This “correct behavior” is arguably not immediately obvious, and was actually surprising to JavaScript developers, so it deserves some explanation. Before we dive into the magical world of promises and async functions, let’s start with some of the foundations.

这个”正确行为”确实第一眼非常不直观,对于JavaScript程序员众来说可能有点让人吃惊,因此这里解释一下。在我们深入promise和async函数之前,让我们先来看下基础。

Tasks vs. microtasks

On a high level there are tasks and microtasks in JavaScript. Tasks handle events like I/O and timers, and execute one at a time. Microtasks implement deferred execution for async/await and promises, and execute at the end of each task. The microtask queue is always emptied before execution returns to the event loop.

就设计来说,JavaScript中存在taskmicrotask。Task负责处理类似I/O以及计时器等事件,且一次只执行一个。Microtask负责处理延后的promise以及async/await执行,且在每个task之后运行。Microtask队列会在返回事件循环之前被清空。

For more details, check out Jake Archibald’s explanation of tasks, microtasks, queues, and schedules in the browser. The task model in Node.js is very similar.

如果想要了解更多细节,请查看Jake Archibald的解释帖 tasks, microtasks, queues, and schedules in the browser。在Node.js中的Task模型也是类似的。

Async functions

According to MDN, an async function is a function which operates asynchronously using an implicit promise to return its result. Async functions are intended to make asynchronous code look like synchronous code, hiding some of the complexity of the asynchronous processing from the developer.

根据MDN,一个async函数就是一个函数会被异步处理,且会使用一个隐式(implicit)Promise来返回结果。Async函数的初衷是使得异步代码看上去像同步代码一样,从开发者这里隐藏掉一部分异步处理的复杂性。

The simplest possible async function looks like this:

最简单的async函数看起来像这样:

async function computeAnswer() {
  return 42;
}

When called it returns a promise, and you can get to its value like with any other promise.

当这个函数被调用的时候,它会返回一个promise,你就可以像处理其他promise一样得到它的返回值。

const p = computeAnswer();
// → Promise

p.then(console.log);
// prints 42 on the next turn

You only get to the value of this promise p the next time microtasks are run. In other words, the above program is semantically equivalent to using Promise.resolve with the value:

你会在下次microtask运行的时候得到这个promisep的返回值。换句话说,上面的程序其实相当于使用Promise.resolve来处理返回值:

function computeAnswer() {
  return Promise.resolve(42);
}

The real power of async functions comes from await expressions, which cause the function execution to pause until a promise is resolved, and resume after fulfillment. The value of await is that of the fulfilled promise. Here’s an example showing what that means:

async函数的真正强大之处体现在await表达式,它会将函数执行暂停在那里,直到promise得到resolve,并会在promise得到fulfillment之后继续执行。await的结果是promise被满足之后的正确结果。下面有一个例子:

async function fetchStatus(url) {
  const response = await fetch(url);
  return response.status;
}

The execution of fetchStatus gets suspended on the await, and is later resumed when the fetch promise fulfills. This is more or less equivalent to chaining a handler onto the promise returned from fetch.

fetchStatus函数的执行会在await处暂停,并会在fetchpromise得到满足之后继续执行。这多多少少有点类似于将一个handler附加到fetch返回的promise之上。

function fetchStatus(url) {
  return fetch(url).then(response => response.status);
}

That handler contains the code following the await in the async function.

这个handler含有在async函数中await之后的代码功能。

Normally you’d pass a Promise to await, but you can actually wait on any arbitrary JavaScript value. If the value of the expression following the await is not a promise, it’s converted to a promise. That means you can await 42 if you feel like doing that:

一般来说你需要将Promise提供给await,但实际上你可以在任何JavaScript值上进行等待。如果await所接的表达式不是一个promise,它就会被转换成一个promise。这意味着你可以编写类似于await 42这样的代码,只要你想:

async function foo() {
  const v = await 42;
  return v;
}

const p = foo();
// → Promise

p.then(console.log);
// prints `42` eventually

More interestingly, await works with any “thenable”, i.e. any object with a then method, even if it’s not a real promise. So you can implement funny things like an asynchronous sleep that measures the actual time spent sleeping:

更有趣的是,await能和任何“thenable”协同工作,举例来说,任何带有then方法的对象,即便它不是一个真正的promise。所以你可以实现很有趣的东西,比如说一个异步睡眠逻辑,并在这个睡眠中记录睡眠时长:

class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(() => resolve(Date.now() - startTime),
               this.timeout);
  }
}

(async () => {
  const actualTime = await new Sleep(1000);
  console.log(actualTime);
})();

Let’s see what V8 does for await under the hood, following the specification. Here’s a simple async function foo:

让我们看看V8在台面下究竟针对await做了什么,参照specification。这里有一个简单的async函数foo

async function foo(v) {
  const w = await v;
  return w;
}

When called, it wraps the parameter v into a promise and suspends execution of the async function until that promise is resolved. Once that happens, execution of the function resumes and w gets assigned the value of the fulfilled promise. This value is then returned from the async function.

当被调用的时候,函数将参数v包装成一个promise,并将执行暂停在那里直到promise得到resolve。此时,函数的执行会恢复,且w得到完成的promise提供的值作为赋值。这个值接下来会从async函数中得到返回。

await under the hood

First of all, V8 marks this function as resumable, which means that execution can be suspended and later resumed (at await points). Then it creates the so-called implicit_promise, which is the promise that is returned when you invoke the async function, and that eventually resolves to the value produced by the async function.

首先,V8将这个函数标记成可恢复,这意味着这个函数的执行能被暂停且能后续被恢复(在await代码点)。然后它会创建被称为隐式 promiseimplicit_promise),就是调用async函数时被返回的promise,并最终被resolve成async函数处理完成之后的值。

Then comes the interesting bit: the actual await. First the value passed to await is wrapped into a promise. Then, handlers are attached to this wrapped promise to resume the function once the promise is fulfilled, and execution of the async function is suspended, returning the implicit_promise to the caller. Once the promise is fulfilled, execution of the async function is resumed with the value w from the promise, and the implicit_promise is resolved with w.

然后就是比较有趣的部分了:真正的await。首先被传递给await的值被包装成一个promise。然后,handlers被附加到这个被包装的promise上用以在promise被满足之后恢复函数的执行,之后async函数的执行就被暂停下来,将隐式 promise返回给调用者。一旦当promise被满足,async函数的执行就会被恢复,带有值wpromise被返回,然后隐式 promise将会被值wresolve。

In a nutshell, the initial steps for await v are:

概括来说,await v的初始步骤如下:

  1. Wrap v — the value passed to await — into a promise.
  2. Attach handlers for resuming the async function later.
  3. Suspend the async function and return the implicit_promise to the caller.
  • 将传给await的值v包装成一个promise
  • 将handlers附加到promise上,为了后续恢复async函数的执行
  • 暂停async函数,然后将隐式 promise返回给调用者

Let’s go through the individual operations step by step. Assume that the thing that is being awaited is already a promise, which was fulfilled with the value 42. Then the engine creates a new promise and resolves that with whatever’s being awaited. This does deferred chaining of these promises on the next turn, expressed via what the specification calls a PromiseResolveThenableJob.

让我们将单独的操作一步步过一下。假设被await暂停的东西已经是一个promise,并被值42满足。然后引擎会创建一个新的promise,并用无论是什么只要是被await的东西来resolve。这个行为将会将这些promise链推迟到下一个回合执行,用specification中的话来表达的话,就是PromiseResolveThenableJob

Then the engine creates another so-called throwaway promise. It’s called throwaway because nothing is ever chained to it — it’s completely internal to the engine. This throwaway promise is then chained onto the promise, with appropriate handlers to resume the async function. This performPromiseThen operation is essentially what Promise.prototype.then() does, behind the scenes. Finally, execution of the async function is suspended, and control returns to the caller.

接下来引擎就创建了一个被称为throwaway的promise。它被称为throwaway是因为没有任何东西被链在它上面 - 它完全内置于引擎内。这个throwawaypromise接下来被链到之前的所说的附有handlers用来恢复async函数执行的promise之上。这个performPromiseThen操作,本质上就是Promise.prototype.then()在底层所做的事情。最终,async函数的执行被暂停,应用的控制被返回给调用者。

Execution continues in the caller, and eventually the call stack becomes empty. Then the JavaScript engine starts running the microtasks: it runs the previously scheduled PromiseResolveThenableJob, which schedules a new PromiseReactionJob to chain the promise onto the value passed to await. Then, the engine returns to processing the microtask queue, since the microtask queue must be emptied before continuing with the main event loop.

应用的执行在调用者这里继续,最终调用栈(call stack)会被清空。接下来JavaScript引擎开始处理microtask:开始运行之前计划的PromiseResolveThenableJob,而这个job则计划了另一个新的PromiseReactionJob将promise链接到交付给await的值。之后,引擎返回处理microtask队列,因为microtask队列必须在下一轮主事件(main event loop)之前清空。

Next up is the PromiseReactionJob, which fulfills the promise with the value from the promise we’re awaiting — 42 in this case — and schedules the reaction onto the throwaway promise. The engine then returns to the microtask loop again, which contains a final microtask to be processed.

接下来就是PromiseReactionJob,它会用我们正在等待的promise中的值来完成promise(在这个例子中,这个值是42),然后安排计划任务,将后续移交到throwawaypromise。引擎接下来会再一次返回处理microtask队列,此时这个队列里有一个最终microtask等待处理。

Now this second PromiseReactionJob propagates the resolution to the throwaway promise, and resumes the suspended execution of the async function, returning the value 42 from the await.

现在,这第二个PromiseReactionJob将后续传播到throwawaypromise,并恢复async函数的执行,从await中返回值42

Summarizing what we’ve learned, for each await the engine has to create two additional promises (even if the right hand side is already a promise) and it needs at least three microtask queue ticks. Who knew that a single await expression resulted in that much overhead?!

总结下我们了解到的信息,对每一个await来说引擎都必须为它创建两个额外的promise(即便表达式的右侧已经是一个promise了),此外,它还需要至少三个microtask队列tick。谁能想到一个简单的await表达式最终需要如此之多的额外开销?!

Let’s have a look at where this overhead comes from. The first line is responsible for creating the wrapper promise. The second line immediately resolves that wrapper promise with the awaited value v. These two lines are responsible for one additional promise plus two out of the three microticks. That’s quite expensive if v is already a promise (which is the common case, since applications normally await on promises). In the unlikely case that a developer awaits on e.g. 42, the engine still needs to wrap it into a promise.

让我们看下这些消耗来自哪里。第一行代码负责创建一个wrapper promise。第二行马上就用await的返回值vresolve了这个promise。这两行导致了一个额外的promise以及总共三个额外的microtick中的两个。如果v已经是一个promise的话,这代价就相当昂贵了(而且这还是常见情况,因为应用程序一般只会对promise进行await)。对于某些不常见的await情况来说,例如42,引擎仍旧需要把它包装成一个promise。

As it turns out, there’s already a promiseResolve operation in the specification that only performs the wrapping when needed:

事实证明,根据specification要求只需要在必要的时候创建一个promiseResolve即可:

This operation returns promises unchanged, and only wraps other values into promises as necessary. This way you save one of the additional promises, plus two ticks on the microtask queue, for the common case that the value passed to await is already a promise. This new behavior is already enabled by default in V8 v7.2. For V8 v7.1, the new behavior can be enabled using the --harmony-await-optimization flag. We’ve proposed this change to the ECMAScript specification as well; the patch is supposed to be merged once we are sure that it’s web-compatible.

这个操作仍旧会返回一个promise,并仅仅只在必要的时候将值包装转换成promise。对于常见情况即将promise直接传递给await,这会节约一个额外的promise开销,再加上两个在microtask队列上的tick。这个新行为在V8 v7.2上已经默认开启。对于V8 v7.1,这个新行为可以通过--harmony-await-optimization选项手动开启。我们也将这个改动作为建议提交到了ECMAScript的specification;一旦当我们确信这个改动是对WEB兼容的,这个补丁就会被合并进去。

Here’s how the new and improved await works behind the scenes, step by step:

下面是新的改进后的await如何工作的,一步一步:

Let’s assume again that we await a promise that was fulfilled with 42. Thanks to the magic of promiseResolve the promise now just refers to the same promise v, so there’s nothing to do in this step. Afterwards the engine continues exactly like before, creating the throwaway promise, scheduling a PromiseReactionJob to resume the async function on the next tick on the microtask queue, suspending execution of the function, and returning to the caller.

让我们再次举例我们会await一个被值42满足的promise。感谢promiseResolve的魔法,promise现在只需要引用到同样是一个promise的v,因此在这步上我们不需要做任何事情。之后引擎做的事情和之前完全相同,创建throwawaypromise,在microtask队列上计划一个PromiseReactionJob在下一个tick上恢复async函数的执行,暂停函数的执行,并将其返回给调用者。

Then eventually when all JavaScript execution finishes, the engine starts running the microtasks, so it executes the PromiseReactionJob. This job propagates the resolution of promise to throwaway, and resumes the execution of the async function, yielding 42 from the await.

最终当JavaScript执行完成的时候,引擎开始运行microtask,然后就执行了PromiseReactionJob。这个job将promise的结果传递给throwaway,然后继续async函数的执行,将await的结果设为42

This optimization avoids the need to create a wrapper promise if the value passed to await is already a promise, and in that case we go from a minimum of three microticks to just one microtick. This behavior is similar to what Node.js 8 does, except that now it’s no longer a bug — it’s now an optimization that is being standardized!

这次优化避免了当传给await的已经是一个promise的情况下,创建一个promise的wrapper,并将最佳case情况下我们需要创建最少3个microtask减少为仅仅一个microtask。这个行为与Node.js 8的时候类似,当然当前的优化并不是一个bug - 现在它是一个被标准化了的优化!

It still feels wrong that the engine has to create this throwaway promise, despite being completely internal to the engine. As it turns out, the throwaway promise was only there to satisfy the API constraints of the internal performPromiseThen operation in the spec.

虽然引擎必须创建一个throwawaypromise的行为看起来也不太正确,即便这个promise仅仅内置于引擎内部。这是因为,这个throwawaypromise的存在意义仅仅只是为了满足spec上对performPromiseThen操作的API要求。

This was recently addressed in an editorial change to the ECMAScript specification. Engines no longer need to create the throwaway promise for await — most of the time.

这件事情最近已经落实到ECMAScript specification的编辑改动(editorial change)中。引擎将不再需要为了await创建一个throwawaypromise - 在大部分情况下。

Comparing await in Node.js 10 to the optimized await that’s likely going to be in Node.js 12 shows the performance impact of this change:

让我们比较下在Node.js 10中的await,以及得到优化并很可能会在Node.js 12中出现的await之间的性能差:

async/await outperforms hand-written promise code now. The key takeaway here is that we significantly reduced the overhead of async functions — not just in V8, but across all JavaScript engines, by patching the spec.

async/await现在在性能上已经优于手写的promise代码了。这是因为我们显著降低了async函数的开销 - 不仅仅是在V8中,还贯穿了所有的JavaScript引擎,通过这个spec:

As mentioned, the patch hasn’t been merged into the ECMAScript specification just yet. The plan is to do so once we’ve made sure that the change doesn’t break the web.

如上面提到的,这个补丁当前已经被合并进入ECMAScript specification中过了。后续的计划是,当我们确信这样做不会破坏WEB兼容性之后,我们就会按spec进行改动。

Improved developer experience

In addition to performance, JavaScript developers also care about the ability to diagnose and fix problems, which is not always easy when dealing with asynchronous code. Chrome DevTools supports async stack traces, i.e. stack traces that not only include the current synchronous part of the stack, but also the asynchronous part:

在性能之外,JavaScript开发者也关注定位及修复问题的能力,对处理异步代码来说这并不简单。Chrome DevTools提供异步堆栈追踪,举例来说:堆栈追踪不仅仅包含当前同步代码的堆栈,也包含了异步的部分:

This is an incredibly useful feature during local development. However, this approach doesn’t really help you once the application is deployed. During post-mortem debugging, you’ll only see the Error#stack output in your log files, and that doesn’t tell you anything about the asynchronous parts.

当在进行本地开发的时候这是令人感到惊异的好用功能。然而,这个功能对上线部署之后的应用程序无能为力。在事后查错(post-mortem debugging)过程中,你就只能在你的日志文件中看Error#stack的输出了,且这部分日志不会包含任何异步逻辑内容。

We’ve recently been working on zero-cost async stack traces which enrich the Error#stack property with async function calls. “Zero-cost” sounds exciting, doesn’t it? How can it be zero-cost, when the Chrome DevTools feature comes with major overhead? Consider this example where foo calls bar asynchronously, and bar throws an exception after awaiting a promise:

我们最近在制作零代价异步堆栈追踪功能,它会丰富Error#stack属性,往里面添加异步函数调用信息。“零代价”听上去让人兴奋,不是么?Chrome DevTools功能已经开销非常大了,那么这个零代价功能又是怎么实现的呢?让我们看下这个例子,foo异步调用bar,然后barawait一个promise之后抛出了一个异常:

async function foo() {
  await bar();
  return 42;
}

async function bar() {
  await Promise.resolve();
  throw new Error('BEEP BEEP');
}

foo().catch(error => console.log(error.stack));

Running this code in Node.js 8 or Node.js 10 results in the following output:

在Node.js 8或Node.js 10中运行这段代码会返回如下输出:

$ node index.js
Error: BEEP BEEP
    at bar (index.js:8:9)
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
    at startup (internal/bootstrap/node.js:266:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)

Note that although the call to foo() causes the error, foo is not part of the stack trace at all. This makes it tricky for JavaScript developers to perform post-mortem debugging, independent of whether your code is deployed in a web application or inside of some cloud container.

请注意看,尽管调用foo()这个行为导致了错误,foo并没有显示在错误堆栈中。这使得JavaScript开发者非常难进行事后查错,除非你的代码是部署在WEB应用中的,或者内置在某些云服务容器内。

The interesting bit here is that the engine knows where it has to continue when bar is done: right after the await in function foo. Coincidentally, that’s also the place where the function foo was suspended. The engine can use this information to reconstruct parts of the asynchronous stack trace, namely the await sites. With this change, the output becomes:

有趣的是,引擎知道当bar结束之后,应该如何继续程序的运行:就在foo函数await部分之后。巧的是,这也是函数foo被暂停的地方。引擎可以使用这个信息来重现异步调用栈的信息,也就是await的部分。当我们改动之后,输出就成了:

$ node --async-stack-traces index.js
Error: BEEP BEEP
    at bar (index.js:8:9)
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
    at startup (internal/bootstrap/node.js:266:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)
    at async foo (index.js:2:3)

In the stack trace, the topmost function comes first, followed by the rest of the synchronous stack trace, followed by the asynchronous call to bar in function foo. This change is implemented in V8 behind the new --async-stack-traces flag. Update: As of V8 v7.3, --async-stack-traces is enabled by default.

在调用栈中,顶端函数最先被打印出来,然后是其余的异步调用栈,紧跟在foobar的异步调用之后。这个改动已经在V8中,但需要添加新的--async-stack-traces选项才能被启用。更新:自V8 v7.3开始,--async-stack-traces已经被默认开启。

However, if you compare this to the async stack trace in Chrome DevTools above, you’ll notice that the actual call site to foo is missing from the asynchronous part of the stack trace. As mentioned before, this approach utilizes the fact that for await the resume and suspend locations are the same — but for regular Promise#then() or Promise#catch() calls, this is not the case. For more background, see Mathias Bynens’s explanation on why await beats Promise#then().

然而,如果你将这个输出和上面提到的Chrome DevTools的异步调用栈输出进行比对,你会发现真正的foo调用在异步部分的调用栈中是缺失的。如之前提到的,这个实现利用了await的暂停和恢复地点是一致的这一特性 - 但对常规的Promise#then()Promise#catch()调用,这就不行了。如果想更深入了解的话,请阅读Mathias Bynens的解释:why await beats Promise#then()

Conclusion

We made async functions faster thanks to two significant optimizations:

感谢两项巨大的优化,我们将async函数优化得更快了:

  • the removal of two extra microticks, and

  • the removal of the throwaway promise.

  • 移除了两个需要额外开销的microtick

  • 移除了throwawaypromise

On top of that, we’ve improved the developer experience via zero-cost async stack traces, which work with await in async functions and Promise.all().

除此之外,我们通过零代价异步堆栈追踪优化了开发者体验,这项优化对async函数中的await以及Promise.all()有效。

And we also have some nice performance advice for JavaScript developers:

然后,我们也有一些对JavaScript开发者很有用的性能优化建议:

  • favor async functions and await over hand-written promise code, and

  • stick to the native promise implementation offered by the JavaScript engine to benefit from the shortcuts, i.e. avoiding two microticks for await.

  • 请使用async函数,使用await,而不是使用手写的promise代码

  • 请使用JavaScript引擎提供的原生(native)promise实现,以便享受性能优化,举例来说:避免await的额外两个microtick开销

EOF

Published 2019/2/14

Some tech & personal blog posts