dawn_node: Add binding/AsyncRunner

Used to poll a wgpu::Device with calls to Tick() while there are asynchronous tasks in flight.

Bug: dawn:1123
Change-Id: Ieee75b983df836a6df09ae4ff81f7382f4be4995
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/64905
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Austin Eng <enga@chromium.org>
diff --git a/src/dawn_node/binding/AsyncRunner.cpp b/src/dawn_node/binding/AsyncRunner.cpp
new file mode 100644
index 0000000..7162b2e
--- /dev/null
+++ b/src/dawn_node/binding/AsyncRunner.cpp
@@ -0,0 +1,55 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/dawn_node/binding/AsyncRunner.h"
+
+#include <cassert>
+#include <limits>
+
+namespace wgpu { namespace binding {
+
+    AsyncRunner::AsyncRunner(Napi::Env env, wgpu::Device device) : env_(env), device_(device) {
+    }
+
+    void AsyncRunner::Begin() {
+        assert(count_ != std::numeric_limits<decltype(count_)>::max());
+        if (count_++ == 0) {
+            QueueTick();
+        }
+    }
+
+    void AsyncRunner::End() {
+        assert(count_ > 0);
+        count_--;
+    }
+
+    void AsyncRunner::QueueTick() {
+        // TODO(crbug.com/dawn/1127): We probably want to reduce the frequency at which this gets
+        // called.
+        env_.Global()
+            .Get("setImmediate")
+            .As<Napi::Function>()
+            .Call({
+                // TODO(crbug.com/dawn/1127): Create once, reuse.
+                Napi::Function::New(env_,
+                                    [this](const Napi::CallbackInfo&) {
+                                        if (count_ > 0) {
+                                            device_.Tick();
+                                            QueueTick();
+                                        }
+                                    }),
+            });
+    }
+
+}}  // namespace wgpu::binding
diff --git a/src/dawn_node/binding/AsyncRunner.h b/src/dawn_node/binding/AsyncRunner.h
new file mode 100644
index 0000000..83644c0
--- /dev/null
+++ b/src/dawn_node/binding/AsyncRunner.h
@@ -0,0 +1,76 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef DAWN_NODE_BINDING_ASYNC_RUNNER_H_
+#define DAWN_NODE_BINDING_ASYNC_RUNNER_H_
+
+#include <stdint.h>
+#include <memory>
+
+#include "dawn/webgpu_cpp.h"
+#include "napi.h"
+
+namespace wgpu { namespace binding {
+
+    // AsyncRunner is used to poll a wgpu::Device with calls to Tick() while there are asynchronous
+    // tasks in flight.
+    class AsyncRunner {
+      public:
+        AsyncRunner(Napi::Env env, wgpu::Device device);
+
+        // Begin() should be called when a new asynchronous task is started.
+        // If the number of executing asynchronous tasks transitions from 0 to 1, then a function
+        // will be scheduled on the main JavaScript thread to call wgpu::Device::Tick() whenever the
+        // thread is idle. This will be repeatedly called until the number of executing asynchronous
+        // tasks reaches 0 again.
+        void Begin();
+
+        // End() should be called once the asynchronous task has finished.
+        // Every call to Begin() should eventually result in a call to End().
+        void End();
+
+      private:
+        void QueueTick();
+        Napi::Env env_;
+        wgpu::Device const device_;
+        uint64_t count_ = 0;
+    };
+
+    // AsyncTask is a RAII helper for calling AsyncRunner::Begin() on construction, and
+    // AsyncRunner::End() on destruction.
+    class AsyncTask {
+      public:
+        inline AsyncTask(AsyncTask&&) = default;
+
+        // Constructor.
+        // Calls AsyncRunner::Begin()
+        inline AsyncTask(std::shared_ptr<AsyncRunner> runner) : runner_(std::move(runner)) {
+            runner_->Begin();
+        };
+
+        // Destructor.
+        // Calls AsyncRunner::End()
+        inline ~AsyncTask() {
+            runner_->End();
+        }
+
+      private:
+        AsyncTask(const AsyncTask&) = delete;
+        AsyncTask& operator=(const AsyncTask&) = delete;
+        std::shared_ptr<AsyncRunner> runner_;
+    };
+
+}}  // namespace wgpu::binding
+
+#endif  // DAWN_NODE_BINDING_ASYNC_RUNNER_H_
diff --git a/src/dawn_node/binding/CMakeLists.txt b/src/dawn_node/binding/CMakeLists.txt
index c12f86e..faad894 100644
--- a/src/dawn_node/binding/CMakeLists.txt
+++ b/src/dawn_node/binding/CMakeLists.txt
@@ -13,6 +13,8 @@
 # limitations under the License.
 
 add_library(dawn_node_binding STATIC
+    "AsyncRunner.cpp"
+    "AsyncRunner.h"
     "Errors.cpp"
     "Errors.h"
 )