Reland "Apply auto expiry mechanism to GPUExternalTexture"

This reland commit ad2a6bfd37ef8a2d8c6eeea447a8ace302fb99cd.

Fix the test case issues.

Original change's description:
> Revert "Apply auto expiry mechanism to GPUExternalTexture"
>
> This reverts commit ad2a6bfd37ef8a2d8c6eeea447a8ace302fb99cd.

> Reason for revert: breaks WebCodecs_GPUExternalTexture_*
> https://luci-milo.appspot.com/ui/p/chromium/builders/ci/Win10%20FYI%20x64%20Release%20(Intel)/956/overview

>> Original change's description:
>> Apply auto expiry mechanism to GPUExternalTexture
>>
>> WebGPU spec updated GPUExternalTexture expire mechanism. It removes
>> GPUExternalTexture.expire and uses auto expiry mechanism.
>>
>> The basic rules are:
>> - ImportExternalTexture should return same object if underly
>>   resource is the same.
>> - GPUExternalTexture should be expire if current task scope finished.
>>   And could be refreshed by importExternalTexture in new task scope.
>>
>> This CL adds auto expiry mechanism and a cache to address these requirements.
>>
>> Bug: chromium:1412338
>> Change-Id: I6ca72d2ea9a013d7729072cb76cb412ecf3a1e86
>> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4258147
>> Commit-Queue: Shaobo Yan <shaobo.yan@intel.com>
>> Reviewed-by: Corentin Wallez <cwallez@chromium.org>
>> Cr-Commit-Position: refs/heads/main@{#1112071}
>
> Bug: chromium:1412338
> Change-Id: I1a9c0b6a2903c6b1e11e2532e4f3ad81081603c8
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4303861
> Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
> Commit-Queue: Yuly Novikov <ynovikov@chromium.org>
> Reviewed-by: Corentin Wallez <cwallez@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1112220}

Bug:chromium:1412338

Change-Id: Ifb8ee07894880b249559999599dc386170760c98
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4310302
Commit-Queue: Shaobo Yan <shaobo.yan@intel.com>
Reviewed-by: Brandon Jones <bajones@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1114424}
diff --git a/content/test/content_unittests_bundle_data.filelist b/content/test/content_unittests_bundle_data.filelist
index 1ce2926..06ffe59 100644
--- a/content/test/content_unittests_bundle_data.filelist
+++ b/content/test/content_unittests_bundle_data.filelist
@@ -792,8 +792,6 @@
 data/gpu/webcodecs/encode-decode.html
 data/gpu/webcodecs/encode.html
 data/gpu/webcodecs/encoding-modes.html
-data/gpu/webcodecs/gpu-device-destroy-expire-active-external-texture.html
-data/gpu/webcodecs/gpu-external-texture-expired.html
 data/gpu/webcodecs/svc.html
 data/gpu/webcodecs/tex-image-2d.html
 data/gpu/webcodecs/webcodecs_common.js
diff --git a/content/test/data/gpu/webcodecs/gpu-device-destroy-expire-active-external-texture.html b/content/test/data/gpu/webcodecs/gpu-device-destroy-expire-active-external-texture.html
deleted file mode 100644
index da6d810..0000000
--- a/content/test/data/gpu/webcodecs/gpu-device-destroy-expire-active-external-texture.html
+++ /dev/null
@@ -1,60 +0,0 @@
-<!DOCTYPE html>
-<!--
-Take frames coming from various sources and render them to a canvas with
-WebGLRenderingContext.texImage2D().
--->
-<html>
-
-<head>
-  <title>GPUDevice.destroy() expires GPUExternalTexture test</title>
-  <script src="webcodecs_common.js"></script>
-  <script type="text/javascript">
-    'use strict';
-    async function main(arg) {
-      const device_destroyed_before_import = arg.device_destroyed_before_import;
-      let source_type = arg.source_type;
-      let canvas = document.getElementById('display');
-      let source =
-        await createFrameSource(source_type, canvas.width, canvas.height);
-      if (!source) {
-        TEST.skip('Unsupported source: ' + source_type);
-        return;
-      }
-
-      const adapter = navigator.gpu && await navigator.gpu.requestAdapter();
-      if (!adapter) {
-        TEST.skip('navigator.gpu && navigator.gpu.requestAdapter failed');
-        return;
-      }
-
-      const device = await adapter.requestDevice();
-      if (!device) {
-        TEST.skip('adapter.requestDevice() failed');
-        return;
-      }
-
-      let frame = await source.getNextFrame();
-      if (device_destroyed_before_import) {
-        device.destroy();
-        const gpu_external_texture = device.importExternalTexture({source: frame});
-        TEST.assert(gpu_external_texture.expired == true, "GPUExternalTexture should be expired");
-      } else {
-        const gpu_external_texture = device.importExternalTexture({source: frame});
-        TEST.assert(gpu_external_texture.expired == false, "GPUExternalTexture should be active");
-        device.destroy();
-        TEST.assert(gpu_external_texture.expired == true, "GPUExternalTexture should be expired");
-      }
-
-      frame.close();
-      source.close();
-    }
-  </script>
-</head>
-
-<body>
-  <div>
-    <canvas id='display' width="640" height="480"></canvas>
-  </div>
-</body>
-
-</html>
\ No newline at end of file
diff --git a/content/test/data/gpu/webcodecs/gpu-external-texture-expired.html b/content/test/data/gpu/webcodecs/gpu-external-texture-expired.html
deleted file mode 100644
index c7e39a2..0000000
--- a/content/test/data/gpu/webcodecs/gpu-external-texture-expired.html
+++ /dev/null
@@ -1,72 +0,0 @@
-<!DOCTYPE html>
-<!--
-Take frames coming from various sources and render them to a canvas with
-WebGLRenderingContext.texImage2D().
--->
-<html>
-
-<head>
-  <title>GPUExternalTexture.expired test</title>
-  <script src="webcodecs_common.js"></script>
-  <script id="myWorker" type="text/worker">
-    self.onmessage = function(e) {
-      self.postMessage("");
-    }
-  </script>
-  <script type="text/javascript">
-    'use strict';
-    function makeWorker(script) {
-      var blob = new Blob([script]);
-      return new Worker(URL.createObjectURL(blob));
-    }
-
-    async function main(arg) {
-      const use_worker = arg.use_worker;
-      let source_type = arg.source_type;
-      let canvas = document.getElementById('display');
-      let source =
-        await createFrameSource(source_type, canvas.width, canvas.height);
-      if (!source) {
-        TEST.skip('Unsupported source: ' + source_type);
-        return;
-      }
-
-      const adapter = navigator.gpu && await navigator.gpu.requestAdapter();
-      if (!adapter) {
-        TEST.skip('navigator.gpu && navigator.gpu.requestAdapter failed');
-        return;
-      }
-
-      const device = await adapter.requestDevice();
-      if (!device) {
-        TEST.skip('adapter.requestDevice() failed');
-        return;
-      }
-
-      let frame = await source.getNextFrame();
-      const gpu_external_texture = device.importExternalTexture({ source: frame });
-      TEST.assert(gpu_external_texture.expired == false, "GPUExternalTexture should be active");
-
-      if (use_worker) {
-        let worker = makeWorker(document.getElementById("myWorker").textContent);
-        worker.onmessage = function (e) {
-          TEST.assert(gpu_external_texture.expired == true, "GPUExternalTexture should be expired");
-        }
-        // GPUExternalTexture should be expired when the frame in the same thread has been transferred.
-        worker.postMessage({ videoFrame: frame }, [frame]);
-      } else {
-        frame.close();
-        TEST.assert(gpu_external_texture.expired == true, "GPUExternalTexture should be expired");
-      }
-      source.close();
-    }
-  </script>
-</head>
-
-<body>
-  <div>
-    <canvas id='display' width="640" height="480"></canvas>
-  </div>
-</body>
-
-</html>
\ No newline at end of file
diff --git a/content/test/gpu/gpu_tests/test_expectations/webcodecs_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webcodecs_expectations.txt
index e27ef26..0de74e0 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webcodecs_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webcodecs_expectations.txt
@@ -110,12 +110,8 @@
 # Camera tests fail with NotFoundError on Sherlock devices.
 crbug.com/1400512 [ fuchsia fuchsia-board-sherlock ] WebCodecs_DrawImage_camera [ Failure ]
 crbug.com/1400512 [ fuchsia fuchsia-board-sherlock ] WebCodecs_Encode_camera_* [ Failure ]
-crbug.com/1400512 [ fuchsia fuchsia-board-sherlock ] WebCodecs_GPUExternalTexture_expired_camera [ Failure ]
-crbug.com/1400512 [ fuchsia fuchsia-board-sherlock ] WebCodecs_GPUExternalTexture_expired_worker_camera [ Failure ]
 crbug.com/1400512 [ fuchsia fuchsia-board-sherlock ] WebCodecs_TexImage2d_camera [ Failure ]
 crbug.com/1400512 [ fuchsia fuchsia-board-sherlock ] WebCodecs_copyTo_camera [ Failure ]
-crbug.com/1400512 [ fuchsia fuchsia-board-sherlock ] WebCodecs_device_destroy_expired_texture_camera [ Failure ]
-crbug.com/1400512 [ fuchsia fuchsia-board-sherlock ] WebCodecs_texture_expired_from_destroyed_device_camera [ Failure ]
 
 # finder:enable-unused
 
diff --git a/content/test/gpu/gpu_tests/webcodecs_integration_test.py b/content/test/gpu/gpu_tests/webcodecs_integration_test.py
index 7692484..48510a3 100644
--- a/content/test/gpu/gpu_tests/webcodecs_integration_test.py
+++ b/content/test/gpu/gpu_tests/webcodecs_integration_test.py
@@ -56,30 +56,6 @@
           'source_type':
           source_type
       }])
-      yield ('WebCodecs_GPUExternalTexture_expired_' + source_type,
-             'gpu-external-texture-expired.html', [{
-                 'source_type': source_type,
-                 'use_worker': False
-             }])
-      yield ('WebCodecs_GPUExternalTexture_expired_worker_' + source_type,
-             'gpu-external-texture-expired.html', [{
-                 'source_type': source_type,
-                 'use_worker': True
-             }])
-      yield ('WebCodecs_device_destroy_expired_texture_' + source_type,
-             'gpu-device-destroy-expire-active-external-texture.html', [{
-                 'source_type':
-                 source_type,
-                 'device_destroyed_before_import':
-                 False
-             }])
-      yield ('WebCodecs_texture_expired_from_destroyed_device_' + source_type,
-             'gpu-device-destroy-expire-active-external-texture.html', [{
-                 'source_type':
-                 source_type,
-                 'device_destroyed_before_import':
-                 True
-             }])
 
   @classmethod
   def GenerateAudioTests(cls) -> ct.TestGenerator:
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_device.cc b/third_party/blink/renderer/modules/webgpu/gpu_device.cc
index 43e81a9..9d38756 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_device.cc
+++ b/third_party/blink/renderer/modules/webgpu/gpu_device.cc
@@ -10,13 +10,11 @@
 #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_compute_pipeline_descriptor.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_device_descriptor.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_error_filter.h"
-#include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_external_texture_descriptor.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_feature_name.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_query_set_descriptor.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_queue_descriptor.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_render_pipeline_descriptor.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_uncaptured_error_event_init.h"
-#include "third_party/blink/renderer/bindings/modules/v8/v8_union_htmlvideoelement_videoframe.h"
 #include "third_party/blink/renderer/core/dom/dom_exception.h"
 #include "third_party/blink/renderer/core/inspector/console_message.h"
 #include "third_party/blink/renderer/modules/event_target_modules.h"
@@ -169,6 +167,8 @@
 
   if (descriptor->defaultQueue()->hasLabel())
     queue_->setLabel(descriptor->defaultQueue()->label());
+
+  external_texture_cache_ = MakeGarbageCollected<ExternalTextureCache>(this);
 }
 
 GPUDevice::~GPUDevice() {
@@ -418,9 +418,13 @@
   return queue_;
 }
 
+bool GPUDevice::destroyed() const {
+  return destroyed_;
+}
+
 void GPUDevice::destroy(v8::Isolate* isolate) {
   destroyed_ = true;
-  DestroyAllExternalTextures();
+  external_texture_cache_->Destroy();
   // Dissociate mailboxes before destroying the device. This ensures that
   // mailbox operations which run during dissociation can succeed.
   DissociateMailboxes();
@@ -456,24 +460,8 @@
     ScriptState* script_state,
     const GPUExternalTextureDescriptor* descriptor,
     ExceptionState& exception_state) {
-  // Gate VideoFrame importExternalTexture on the WebGPUWebCodecs OT.
-  ExecutionContext* execution_context = ExecutionContext::From(script_state);
-  if (descriptor->source()->GetContentType() ==
-          V8UnionHTMLVideoElementOrVideoFrame::ContentType::kVideoFrame &&
-      !RuntimeEnabledFeatures::WebGPUWebCodecsEnabled(execution_context)) {
-    exception_state.ThrowTypeError(
-        "VideoFrame isn't supported for importExternalTexture. This feature "
-        "requires the WebGPUWebCodecs origin trial or "
-        "--enable-webgpu-developer-features");
-    return nullptr;
-  }
-
-  // Ensure the GPUExternalTexture created from a destroyed GPUDevice will be
-  // expired immediately.
-  if (destroyed_)
-    return GPUExternalTexture::CreateExpired(this, descriptor, exception_state);
-
-  return GPUExternalTexture::Create(this, descriptor, exception_state);
+  return external_texture_cache_->Import(ExecutionContext::From(script_state),
+                                         descriptor, exception_state);
 }
 
 GPUBindGroup* GPUDevice::createBindGroup(
@@ -665,7 +653,7 @@
   visitor->Trace(limits_);
   visitor->Trace(queue_);
   visitor->Trace(lost_property_);
-  visitor->Trace(active_external_textures_);
+  visitor->Trace(external_texture_cache_);
   visitor->Trace(textures_with_mailbox_);
   visitor->Trace(mappable_buffers_);
   ExecutionContextClient::Trace(visitor);
@@ -675,14 +663,7 @@
 void GPUDevice::Dispose() {
   // This call accesses other GC objects, so it cannot be called inside GC
   // objects destructors. Instead call it in the pre-finalizer.
-  DestroyAllExternalTextures();
-}
-
-void GPUDevice::DestroyAllExternalTextures() {
-  for (auto& external_texture : active_external_textures_) {
-    external_texture->Destroy();
-  }
-  active_external_textures_.clear();
+  external_texture_cache_->Destroy();
 }
 
 void GPUDevice::DissociateMailboxes() {
@@ -706,17 +687,6 @@
   mappable_buffers_.erase(buffer);
 }
 
-void GPUDevice::AddActiveExternalTexture(GPUExternalTexture* external_texture) {
-  DCHECK(external_texture);
-  active_external_textures_.insert(external_texture);
-}
-
-void GPUDevice::RemoveActiveExternalTexture(
-    GPUExternalTexture* external_texture) {
-  DCHECK(external_texture);
-  active_external_textures_.erase(external_texture);
-}
-
 void GPUDevice::TrackTextureWithMailbox(GPUTexture* texture) {
   DCHECK(texture);
   textures_with_mailbox_.insert(texture);
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_device.h b/third_party/blink/renderer/modules/webgpu/gpu_device.h
index 584ff28..4ce3c99 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_device.h
+++ b/third_party/blink/renderer/modules/webgpu/gpu_device.h
@@ -18,6 +18,7 @@
 namespace blink {
 
 class ExecutionContext;
+class ExternalTextureCache;
 class HTMLCanvasElement;
 class GPUAdapter;
 class GPUBuffer;
@@ -81,6 +82,7 @@
   ScriptPromise lost(ScriptState* script_state);
 
   GPUQueue* queue();
+  bool destroyed() const;
 
   void destroy(v8::Isolate* isolate);
 
@@ -92,6 +94,7 @@
                                         unsigned int usage_flags,
                                         ExceptionState& exception_state);
   GPUSampler* createSampler(const GPUSamplerDescriptor* descriptor);
+
   GPUExternalTexture* importExternalTexture(
       ScriptState* script_state,
       const GPUExternalTextureDescriptor* descriptor,
@@ -142,9 +145,6 @@
   void InjectError(WGPUErrorType type, const char* message);
   void AddConsoleWarning(const char* message);
 
-  void AddActiveExternalTexture(GPUExternalTexture* external_texture);
-  void RemoveActiveExternalTexture(GPUExternalTexture* external_texture);
-
   void TrackTextureWithMailbox(GPUTexture* texture);
   void UntrackTextureWithMailbox(GPUTexture* texture);
 
@@ -165,8 +165,6 @@
 
   // Used by USING_PRE_FINALIZER.
   void Dispose();
-
-  void DestroyAllExternalTextures();
   void DissociateMailboxes();
   void UnmapAllMappableBuffers(v8::Isolate* isolate);
 
@@ -215,15 +213,13 @@
   static constexpr int kMaxAllowedConsoleWarnings = 500;
   int allowed_console_warnings_remaining_ = kMaxAllowedConsoleWarnings;
 
-  // Keep a list of all active GPUExternalTexture. Eagerly destroy them
-  // when the device is destroyed (via .destroy) to free the memory.
-  HeapHashSet<WeakMember<GPUExternalTexture>> active_external_textures_;
-
   // Textures with mailboxes that should be dissociated before device.destroy().
   HeapHashSet<WeakMember<GPUTexture>> textures_with_mailbox_;
 
   HeapHashSet<WeakMember<GPUBuffer>> mappable_buffers_;
 
+  Member<ExternalTextureCache> external_texture_cache_;
+
   // This attribute records that whether GPUDevice is destroyed (via destroy()).
   bool destroyed_ = false;
 };
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc b/third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc
index 026a917..f8616b2 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc
+++ b/third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc
@@ -14,16 +14,153 @@
 #include "third_party/blink/renderer/modules/webcodecs/video_frame.h"
 #include "third_party/blink/renderer/modules/webgpu/dawn_conversions.h"
 #include "third_party/blink/renderer/modules/webgpu/external_texture_helper.h"
-#include "third_party/blink/renderer/modules/webgpu/gpu_adapter.h"
 #include "third_party/blink/renderer/modules/webgpu/gpu_device.h"
 #include "third_party/blink/renderer/platform/graphics/gpu/webgpu_mailbox_texture.h"
 #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
 
 namespace blink {
+ExternalTextureCache::ExternalTextureCache(GPUDevice* device)
+    : device_(device) {}
+
+GPUExternalTexture* ExternalTextureCache::Import(
+    ExecutionContext* execution_context,
+    const GPUExternalTextureDescriptor* descriptor,
+    ExceptionState& exception_state) {
+  // Gate VideoFrame importExternalTexture on the WebGPUWebCodecs OT.
+  if (descriptor->source()->GetContentType() ==
+          V8UnionHTMLVideoElementOrVideoFrame::ContentType::kVideoFrame &&
+      !RuntimeEnabledFeatures::WebGPUWebCodecsEnabled(execution_context)) {
+    exception_state.ThrowTypeError(
+        "VideoFrame isn't supported for importExternalTexture. This feature "
+        "requires the WebGPUWebCodecs origin trial or "
+        "--enable-webgpu-developer-features");
+    return nullptr;
+  }
+
+  // Ensure the GPUExternalTexture created from a destroyed GPUDevice will be
+  // expired immediately.
+  if (device()->destroyed()) {
+    return GPUExternalTexture::CreateExpired(this, descriptor, exception_state);
+  }
+
+  GPUExternalTexture* external_texture = nullptr;
+  switch (descriptor->source()->GetContentType()) {
+    case V8UnionHTMLVideoElementOrVideoFrame::ContentType::kHTMLVideoElement: {
+      HTMLVideoElement* video = descriptor->source()->GetAsHTMLVideoElement();
+
+      auto cache = from_html_video_element_.find(video);
+      if (cache != from_html_video_element_.end()) {
+        external_texture = cache->value;
+      } else {
+        external_texture = GPUExternalTexture::FromHTMLVideoElement(
+            this, video, descriptor, exception_state);
+      }
+      break;
+    }
+    case V8UnionHTMLVideoElementOrVideoFrame::ContentType::kVideoFrame: {
+      VideoFrame* frame = descriptor->source()->GetAsVideoFrame();
+
+      auto cache = from_video_frame_.find(frame);
+      if (cache != from_video_frame_.end()) {
+        external_texture = cache->value;
+      } else {
+        external_texture = GPUExternalTexture::FromVideoFrame(
+            this, frame, descriptor, exception_state);
+      }
+      break;
+    }
+    default:
+      NOTREACHED();
+  }
+
+  if (external_texture) {
+    external_texture->Refresh();
+    ExpireAtEndOfTask(external_texture);
+  }
+
+  return external_texture;
+}
+
+void ExternalTextureCache::Destroy() {
+  for (auto& cache : from_html_video_element_) {
+    cache.value->Destroy();
+  }
+  from_html_video_element_.clear();
+
+  for (auto& cache : from_video_frame_) {
+    cache.value->Destroy();
+  }
+  from_video_frame_.clear();
+
+  // GPUExternalTexture in expire list should be in from_html_video_element_ and
+  // from_video_frame_. It has been destroyed when clean up the cache. Clear
+  // list here is enough.
+  expire_list_.clear();
+}
+
+void ExternalTextureCache::Add(HTMLVideoElement* video,
+                               GPUExternalTexture* external_texture) {
+  from_html_video_element_.insert(video, external_texture);
+}
+
+void ExternalTextureCache::Remove(HTMLVideoElement* video) {
+  from_html_video_element_.erase(video);
+}
+
+void ExternalTextureCache::Add(VideoFrame* frame,
+                               GPUExternalTexture* external_texture) {
+  from_video_frame_.insert(frame, external_texture);
+}
+
+void ExternalTextureCache::Remove(VideoFrame* frame) {
+  from_video_frame_.erase(frame);
+}
+
+void ExternalTextureCache::Trace(Visitor* visitor) const {
+  visitor->Trace(from_html_video_element_);
+  visitor->Trace(from_video_frame_);
+  visitor->Trace(expire_list_);
+  visitor->Trace(device_);
+}
+
+GPUDevice* ExternalTextureCache::device() const {
+  return device_.Get();
+}
+
+void ExternalTextureCache::ExpireAtEndOfTask(
+    GPUExternalTexture* external_texture) {
+  DCHECK(external_texture);
+  expire_list_.push_back(external_texture);
+
+  if (expire_task_scheduled_) {
+    return;
+  }
+
+  device()
+      ->GetExecutionContext()
+      ->GetTaskRunner(TaskType::kWebGPU)
+      ->PostTask(FROM_HERE, WTF::BindOnce(&ExternalTextureCache::ExpireTask,
+                                          WrapWeakPersistent(this)));
+  expire_task_scheduled_ = true;
+}
+
+void ExternalTextureCache::ExpireTask() {
+  // GPUDevice.destroy() call has destroyed all pending external textures.
+  if (!expire_task_scheduled_) {
+    return;
+  }
+
+  expire_task_scheduled_ = false;
+
+  auto external_textures = std::move(expire_list_);
+  for (auto& external_texture : external_textures) {
+    external_texture->Expire();
+  }
+}
 
 // static
 GPUExternalTexture* GPUExternalTexture::CreateImpl(
-    GPUDevice* device,
+    ExternalTextureCache* cache,
     const GPUExternalTextureDescriptor* webgpu_desc,
     scoped_refptr<media::VideoFrame> media_video_frame,
     media::PaintCanvasVideoRenderer* video_renderer,
@@ -64,7 +201,7 @@
       PredefinedColorSpaceToGfxColorSpace(dst_predefined_color_space);
 
   ExternalTexture external_texture =
-      CreateExternalTexture(device, src_color_space, dst_color_space,
+      CreateExternalTexture(cache->device(), src_color_space, dst_color_space,
                             media_video_frame, video_renderer);
 
   if (external_texture.wgpu_external_texture == nullptr ||
@@ -76,7 +213,7 @@
 
   GPUExternalTexture* gpu_external_texture =
       MakeGarbageCollected<GPUExternalTexture>(
-          device, external_texture.wgpu_external_texture,
+          cache, external_texture.wgpu_external_texture,
           external_texture.mailbox_texture, external_texture.is_zero_copy,
           media_video_frame_unique_id);
 
@@ -85,7 +222,7 @@
 
 // static
 GPUExternalTexture* GPUExternalTexture::CreateExpired(
-    GPUDevice* device,
+    ExternalTextureCache* cache,
     const GPUExternalTextureDescriptor* webgpu_desc,
     ExceptionState& exception_state) {
   // Validate GPUExternalTextureDescriptor.
@@ -108,9 +245,9 @@
   // Bypass importing video frame into Dawn.
   GPUExternalTexture* external_texture =
       MakeGarbageCollected<GPUExternalTexture>(
-          device,
-          device->GetProcs().deviceCreateErrorExternalTexture(
-              device->GetHandle()),
+          cache,
+          cache->device()->GetProcs().deviceCreateErrorExternalTexture(
+              cache->device()->GetHandle()),
           nullptr /*mailbox_texture*/, false /*is_zero_copy*/,
           absl::nullopt /*media_video_frame_unique_id*/);
 
@@ -119,7 +256,7 @@
 
 // static
 GPUExternalTexture* GPUExternalTexture::FromHTMLVideoElement(
-    GPUDevice* device,
+    ExternalTextureCache* cache,
     HTMLVideoElement* video,
     const GPUExternalTextureDescriptor* webgpu_desc,
     ExceptionState& exception_state) {
@@ -129,7 +266,7 @@
     return nullptr;
 
   GPUExternalTexture* external_texture = GPUExternalTexture::CreateImpl(
-      device, webgpu_desc, source.media_video_frame, source.video_renderer,
+      cache, webgpu_desc, source.media_video_frame, source.video_renderer,
       source.media_video_frame_unique_id, exception_state);
 
   // WebGPU Spec requires that If the latest presented frame of video is not
@@ -139,7 +276,7 @@
   // list for management.
   if (external_texture) {
     external_texture->ListenToHTMLVideoElement(video);
-    device->AddActiveExternalTexture(external_texture);
+    cache->Add(video, external_texture);
   }
 
   return external_texture;
@@ -147,7 +284,7 @@
 
 // static
 GPUExternalTexture* GPUExternalTexture::FromVideoFrame(
-    GPUDevice* device,
+    ExternalTextureCache* cache,
     VideoFrame* frame,
     const GPUExternalTextureDescriptor* webgpu_desc,
     ExceptionState& exception_state) {
@@ -157,7 +294,7 @@
     return nullptr;
 
   GPUExternalTexture* external_texture = GPUExternalTexture::CreateImpl(
-      device, webgpu_desc, source.media_video_frame, source.video_renderer,
+      cache, webgpu_desc, source.media_video_frame, source.video_renderer,
       absl::nullopt, exception_state);
 
   // If the webcodec video frame has been closed or destroyed, set expired to
@@ -168,52 +305,49 @@
     external_texture->ListenToVideoFrame(frame);
 
     // VideoFrame maybe closed when GPUExternalTexture trying to listen to.
-    // In that case GPUExternalTexture should be expired and GPUDevice
+    // In that case GPUExternalTexture is not active and GPUDevice
     // doesn't need to manage it.
-    if (!external_texture->expired())
-      device->AddActiveExternalTexture(external_texture);
+    if (external_texture->active()) {
+      cache->Add(frame, external_texture);
+    }
   }
 
   return external_texture;
 }
 
-// static
-GPUExternalTexture* GPUExternalTexture::Create(
-    GPUDevice* device,
-    const GPUExternalTextureDescriptor* webgpu_desc,
-    ExceptionState& exception_state) {
-  switch (webgpu_desc->source()->GetContentType()) {
-    case V8UnionHTMLVideoElementOrVideoFrame::ContentType::kHTMLVideoElement: {
-      HTMLVideoElement* video = webgpu_desc->source()->GetAsHTMLVideoElement();
-      return GPUExternalTexture::FromHTMLVideoElement(
-          device, video, webgpu_desc, exception_state);
-    }
-    case V8UnionHTMLVideoElementOrVideoFrame::ContentType::kVideoFrame: {
-      VideoFrame* frame = webgpu_desc->source()->GetAsVideoFrame();
-      return GPUExternalTexture::FromVideoFrame(device, frame, webgpu_desc,
-                                                exception_state);
-    }
-  }
-
-  NOTREACHED();
-}
-
 GPUExternalTexture::GPUExternalTexture(
-    GPUDevice* device,
+    ExternalTextureCache* cache,
     WGPUExternalTexture external_texture,
     scoped_refptr<WebGPUMailboxTexture> mailbox_texture,
     bool is_zero_copy,
     absl::optional<media::VideoFrame::ID> media_video_frame_unique_id)
-    : DawnObject<WGPUExternalTexture>(device, external_texture),
+    : DawnObject<WGPUExternalTexture>(cache->device(), external_texture),
       mailbox_texture_(mailbox_texture),
       is_zero_copy_(is_zero_copy),
-      media_video_frame_unique_id_(media_video_frame_unique_id) {
+      media_video_frame_unique_id_(media_video_frame_unique_id),
+      cache_(cache) {
   // Mark GPUExternalTexture without back resources as destroyed because no need
   // to do real resource releasing.
   if (!mailbox_texture_)
     status_ = Status::Destroyed;
 }
 
+void GPUExternalTexture::Refresh() {
+  DCHECK(status_ != Status::Destroyed);
+
+  GetProcs().externalTextureRefresh(GetHandle());
+  status_ = Status::Active;
+}
+
+void GPUExternalTexture::Expire() {
+  if (expired() || destroyed()) {
+    return;
+  }
+
+  GetProcs().externalTextureExpire(GetHandle());
+  status_ = Status::Expired;
+}
+
 void GPUExternalTexture::Destroy() {
   DCHECK(!destroyed());
   DCHECK(mailbox_texture_);
@@ -224,15 +358,17 @@
 
 void GPUExternalTexture::ListenToHTMLVideoElement(HTMLVideoElement* video) {
   DCHECK(video);
-
-  video_ = video;
   video->GetDocument()
       .GetScriptedAnimationController()
       .WebGPURegisterVideoFrameStateCallback(WTF::BindRepeating(
           &GPUExternalTexture::ContinueCheckingCurrentVideoFrame,
           WrapPersistent(this)));
 
-  status_ = Status::ListenToHTMLVideoElement;
+  video_ = video;
+  task_runner_ =
+      device()->GetExecutionContext()->GetTaskRunner(TaskType::kWebGPU);
+
+  status_ = Status::Active;
 }
 
 bool GPUExternalTexture::ContinueCheckingCurrentVideoFrame() {
@@ -246,7 +382,7 @@
 
   // HTMLVideoElement transition from having a WMP to not having one.
   if (!media_player) {
-    ExpireExternalTextureFromHTMLVideoElement();
+    OnSourceInvalidated();
     return false;
   }
 
@@ -254,7 +390,7 @@
   // with current video frame from compositor to detect a new presented
   // video frame and expire the GPUExternalTexture.
   if (media_video_frame_unique_id_ != media_player->CurrentFrameId()) {
-    ExpireExternalTextureFromHTMLVideoElement();
+    OnSourceInvalidated();
     return false;
   }
 
@@ -262,22 +398,45 @@
 }
 
 void GPUExternalTexture::Trace(Visitor* visitor) const {
+  visitor->Trace(frame_);
   visitor->Trace(video_);
+  visitor->Trace(cache_);
   DawnObject<WGPUExternalTexture>::Trace(visitor);
 }
 
-void GPUExternalTexture::ExpireExternalTextureFromHTMLVideoElement() {
-  DCHECK(status_ != Status::ListenToVideoFrame);
-  ExpireExternalTexture();
+void GPUExternalTexture::OnSourceInvalidated() {
+  DCHECK(task_runner_);
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  // OnSourceInvalidated is called for both VideoFrame and HTMLVE.
+  // VideoFrames are invalidated with and explicit close() call that
+  // should mark the ExternalTexture destroyed immediately.
+  // However HTMLVE could decide to advance in the middle of the task
+  // that imported the ExternalTexture. In that case defer the invalidation
+  // until the end of the task to preserve the semantic of ExternalTexture.
+  if (status_ == Status::Active && video_) {
+    if (task_runner_->BelongsToCurrentThread()) {
+      task_runner_->PostTask(FROM_HERE,
+                             WTF::BindOnce(&GPUExternalTexture::RemoveFromCache,
+                                           WrapWeakPersistent(this)));
+    } else {
+      task_runner_->PostTask(FROM_HERE,
+                             ConvertToBaseOnceCallback(CrossThreadBindOnce(
+                                 &GPUExternalTexture::RemoveFromCache,
+                                 WrapCrossThreadWeakPersistent(this))));
+    }
+  } else {
+    RemoveFromCache();
+  }
 }
 
-void GPUExternalTexture::ExpireExternalTextureFromVideoFrame() {
-  DCHECK(status_ != Status::ListenToHTMLVideoElement);
-  ExpireExternalTexture();
-}
+void GPUExternalTexture::RemoveFromCache() {
+  if (video_) {
+    cache_->Remove(video_);
+  } else if (frame_) {
+    cache_->Remove(frame_);
+  }
 
-void GPUExternalTexture::ExpireExternalTexture() {
-  device()->RemoveActiveExternalTexture(this);
   Destroy();
 }
 
@@ -290,10 +449,11 @@
     return;
   }
 
+  frame_ = frame;
   task_runner_ =
       device()->GetExecutionContext()->GetTaskRunner(TaskType::kWebGPU);
 
-  status_ = Status::ListenToVideoFrame;
+  status_ = Status::Active;
 }
 
 void GPUExternalTexture::OnVideoFrameClosed() {
@@ -309,7 +469,7 @@
   status_ = Status::Expired;
 
   if (task_runner_->BelongsToCurrentThread()) {
-    ExpireExternalTextureFromVideoFrame();
+    OnSourceInvalidated();
     return;
   }
 
@@ -321,8 +481,12 @@
                              WrapCrossThreadWeakPersistent(this))));
 }
 
+bool GPUExternalTexture::active() const {
+  return status_ == Status::Active;
+}
+
 bool GPUExternalTexture::expired() const {
-  return status_ == Status::Expired || status_ == Status::Destroyed;
+  return status_ == Status::Expired;
 }
 
 bool GPUExternalTexture::isZeroCopy() const {
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_external_texture.h b/third_party/blink/renderer/modules/webgpu/gpu_external_texture.h
index 1714dde..232b2d6 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_external_texture.h
+++ b/third_party/blink/renderer/modules/webgpu/gpu_external_texture.h
@@ -10,30 +10,98 @@
 #include "base/task/single_thread_task_runner.h"
 #include "media/base/video_frame.h"
 #include "third_party/blink/renderer/modules/webgpu/dawn_object.h"
+#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
+#include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h"
+#include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/wtf/ref_counted.h"
 
 namespace blink {
 
 class ExceptionState;
+class GPUExternalTexture;
 class GPUExternalTextureDescriptor;
+class WebGPUMailboxTexture;
 class HTMLVideoElement;
 class VideoFrame;
-class WebGPUMailboxTexture;
+
+// GPUExternalTexture uses auto expiry mechanism
+// (https://www.w3.org/TR/webgpu/#-automatic-expiry-task-source). The
+// mechanism requires webgpu to expire GPUExternalTexture when current task
+// scope finished by posting expiration task. The expired GPUExternalTexture
+// is invalid to submit and needs to call importExternalTexture() to get the
+// refreshed GPUExternalTexture object. In implementation side,
+// importExternalTexture() also wraps GPUExternalTexture with underly video
+// frames. It is possible that multiple importExternalTexture() call with the
+// same source tries to wrap the same underly video frames. So a cache system
+// has been integrated to avoid re-creating the GPUExternalTexture again and
+// again with the same video frame. So importExternalTexture() tries to do:
+// - Search cache to see any hit. if not, create a new GPUExternalTexture and
+// insert it into cache.
+// - Refresh the external texture to un-expire it.
+// - Post a task to expire this external texture after finishing current task.
+// More details refers to
+// https://www.w3.org/TR/webgpu/#external-texture-creation
+class ExternalTextureCache : public GarbageCollected<ExternalTextureCache> {
+ public:
+  explicit ExternalTextureCache(GPUDevice* device);
+  ExternalTextureCache(const ExternalTextureCache&) = delete;
+  ExternalTextureCache& operator=(const ExternalTextureCache&) = delete;
+
+  // Implement importExternalTexture() auto expiry mechanism.
+  GPUExternalTexture* Import(ExecutionContext* execution_context,
+                             const GPUExternalTextureDescriptor* descriptor,
+                             ExceptionState& exception_state);
+
+  // Destroy all cached GPUExternalTexture and clear all lists.
+  void Destroy();
+
+  void Add(HTMLVideoElement* video, GPUExternalTexture* external_texture);
+  void Remove(HTMLVideoElement* video);
+
+  void Add(VideoFrame* frame, GPUExternalTexture* external_texture);
+  void Remove(VideoFrame* frame);
+
+  void Trace(Visitor* visitor) const;
+  GPUDevice* device() const;
+
+ private:
+  void ExpireAtEndOfTask(GPUExternalTexture* external_texture);
+  void ExpireTask();
+
+  // Keep a list of all active GPUExternalTexture. Eagerly destroy them
+  // when the device is destroyed (via .destroy) to free the memory.
+  HeapHashMap<WeakMember<HTMLVideoElement>, WeakMember<GPUExternalTexture>>
+      from_html_video_element_;
+  HeapHashMap<WeakMember<VideoFrame>, WeakMember<GPUExternalTexture>>
+      from_video_frame_;
+
+  bool expire_task_scheduled_ = false;
+  HeapVector<Member<GPUExternalTexture>> expire_list_;
+
+  Member<GPUDevice> device_;
+};
 
 class GPUExternalTexture : public DawnObject<WGPUExternalTexture> {
   DEFINE_WRAPPERTYPEINFO();
 
  public:
-  static GPUExternalTexture* Create(
-      GPUDevice* device,
+  static GPUExternalTexture* CreateExpired(
+      ExternalTextureCache* cache,
       const GPUExternalTextureDescriptor* webgpu_desc,
       ExceptionState& exception_state);
-  static GPUExternalTexture* CreateExpired(
-      GPUDevice* device,
+  static GPUExternalTexture* FromHTMLVideoElement(
+      ExternalTextureCache* cache,
+      HTMLVideoElement* video,
+      const GPUExternalTextureDescriptor* webgpu_desc,
+      ExceptionState& exception_state);
+  static GPUExternalTexture* FromVideoFrame(
+      ExternalTextureCache* cache,
+      VideoFrame* frame,
       const GPUExternalTextureDescriptor* webgpu_desc,
       ExceptionState& exception_state);
   explicit GPUExternalTexture(
-      GPUDevice* device,
+      ExternalTextureCache* cache,
       WGPUExternalTexture external_texture,
       scoped_refptr<WebGPUMailboxTexture> mailbox_texture,
       bool is_zero_copy,
@@ -42,11 +110,12 @@
   GPUExternalTexture(const GPUExternalTexture&) = delete;
   GPUExternalTexture& operator=(const GPUExternalTexture&) = delete;
 
-  void Destroy();
-
-  bool expired() const;
   bool isZeroCopy() const;
 
+  void Destroy();
+  void Expire();
+  void Refresh();
+
   void ListenToHTMLVideoElement(HTMLVideoElement* video);
   void ListenToVideoFrame(VideoFrame* frame);
 
@@ -68,28 +137,19 @@
   void Trace(Visitor* visitor) const override;
 
  private:
-  // The initial state of GPUExternalTexture is Expired. After listening to the
-  // imported HTMLVE/VideoFrame, the state should be set to
-  // ListenToHTMLVideoElement/VideoFrame, and then should only be changed in the
-  // order: ListenToHTMLVideoElement/VideoFrame(->Expired)->Destroyed.
-  enum class Status {
-    ListenToHTMLVideoElement,
-    ListenToVideoFrame,
-    Expired,
-    Destroyed
-  };
-  static GPUExternalTexture* FromHTMLVideoElement(
-      GPUDevice* device,
-      HTMLVideoElement* video,
-      const GPUExternalTextureDescriptor* webgpu_desc,
-      ExceptionState& exception_state);
-  static GPUExternalTexture* FromVideoFrame(
-      GPUDevice* device,
-      VideoFrame* frame,
-      const GPUExternalTextureDescriptor* webgpu_desc,
-      ExceptionState& exception_state);
+  //                     [1]           [2]
+  // Creation -> [Active] --> [Expired] --> [Destroyed]
+  //                ^            |
+  //                |-------------
+  //                     [3]
+  //
+  // [1] Happens when the current task finishes: the GPUExternalTexture cannot
+  // be used util it is refreshed [2] Happens when the source changes frames,
+  // the texture can no longer be refreshed. [3] Happens when the texture is
+  // refreshed by being re-imported.
+  enum class Status { Active, Expired, Destroyed };
   static GPUExternalTexture* CreateImpl(
-      GPUDevice* device,
+      ExternalTextureCache* cache,
       const GPUExternalTextureDescriptor* webgpu_desc,
       scoped_refptr<media::VideoFrame> media_video_frame,
       media::PaintCanvasVideoRenderer* video_renderer,
@@ -101,18 +161,18 @@
     GetProcs().externalTextureSetLabel(GetHandle(), utf8_label.c_str());
   }
 
-  // This is the function to expire the external texture when the imported
-  // Blink::VideoFrame has been closed. The function is used as callback
-  // function and be registered to the imported Blink::VideoFrame.
-  void ExpireExternalTextureFromVideoFrame();
+  // This is the function to push a task to destroy the external texture when
+  // the imported video frame in GPUDevice cache is outdated. The function is
+  // used as callback function and be registered to the imported
+  // Blink::VideoFrame or HTMLVideoElement.
+  void OnSourceInvalidated();
 
-  // This is the function to expire the external texture when the imported
-  // HTMLVideoElement it imported from has been closed. The function is used
-  // as callback function and be registered to the imported HTMLVideoElement.
-  void ExpireExternalTextureFromHTMLVideoElement();
+  // GPUDevice holds cache for GPUExternalTextures to handling import same
+  // frame multiple time cases.
+  void RemoveFromCache();
 
-  void ExpireExternalTexture();
-
+  bool active() const;
+  bool expired() const;
   bool destroyed() const;
 
   scoped_refptr<WebGPUMailboxTexture> mailbox_texture_;
@@ -120,6 +180,8 @@
 
   absl::optional<media::VideoFrame::ID> media_video_frame_unique_id_;
   WeakMember<HTMLVideoElement> video_;
+  WeakMember<VideoFrame> frame_;
+  WeakMember<ExternalTextureCache> cache_;
   scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
 
   std::atomic<Status> status_ = Status::Expired;
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_external_texture.idl b/third_party/blink/renderer/modules/webgpu/gpu_external_texture.idl
index 88b76039..1b9ea67 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_external_texture.idl
+++ b/third_party/blink/renderer/modules/webgpu/gpu_external_texture.idl
@@ -8,7 +8,6 @@
     Exposed(Window WebGPU, DedicatedWorker WebGPU),
     SecureContext
 ] interface GPUExternalTexture {
-    readonly attribute boolean expired;
     [RuntimeEnabled=WebGPUDeveloperFeatures] readonly attribute boolean isZeroCopy;
 };
 GPUExternalTexture includes GPUObjectBase;