Chromium memory instrumentation support in Dawn

Expose an interface to integrate with Chromium memory instrumentation,
and implement memory dumps for a few important object types like buffers
and textures. These implementations are simplistic e.g. using estimated
byte size for textures, but they work cross-platform and per-platform
implementations can be added later e.g. to dump resource heap usage.

Bug: chromium:330806170
Change-Id: I2b7b5f96268b69100d13b635e26bed5ccbcd9792
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/180180
Commit-Queue: Austin Eng <enga@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Reviewed-by: Austin Eng <enga@chromium.org>
diff --git a/include/dawn/native/DawnNative.h b/include/dawn/native/DawnNative.h
index 4b4c147..74749b1 100644
--- a/include/dawn/native/DawnNative.h
+++ b/include/dawn/native/DawnNative.h
@@ -28,6 +28,7 @@
 #ifndef INCLUDE_DAWN_NATIVE_DAWNNATIVE_H_
 #define INCLUDE_DAWN_NATIVE_DAWNNATIVE_H_
 
+#include <string>
 #include <vector>
 
 #include "dawn/dawn_proc_table.h"
@@ -283,6 +284,30 @@
 // name of an feature supported in Dawn.
 DAWN_NATIVE_EXPORT const FeatureInfo* GetFeatureInfo(wgpu::FeatureName feature);
 
+class DAWN_NATIVE_EXPORT MemoryDump {
+  public:
+    // Standard attribute |name|s for the AddScalar() and AddString() methods.
+    // These match the expected names in Chromium memory-infra instrumentation.
+    static const char kNameSize[];         // To represent allocated space.
+    static const char kNameObjectCount[];  // To represent number of objects.
+
+    // Standard attribute |unit|s for the AddScalar() and AddString() methods.
+    // These match the expected names in Chromium memory-infra instrumentation.
+    static const char kUnitsBytes[];    // Unit name to represent bytes.
+    static const char kUnitsObjects[];  // Unit name to represent #objects.
+
+    virtual void AddScalar(const char* name,
+                           const char* key,
+                           const char* units,
+                           uint64_t value) = 0;
+
+    virtual void AddString(const char* name, const char* key, const std::string& value) = 0;
+
+  protected:
+    virtual ~MemoryDump() = default;
+};
+DAWN_NATIVE_EXPORT void DumpMemoryStatistics(WGPUDevice device, MemoryDump* dump);
+
 }  // namespace dawn::native
 
 #endif  // INCLUDE_DAWN_NATIVE_DAWNNATIVE_H_
diff --git a/src/dawn/native/Buffer.cpp b/src/dawn/native/Buffer.cpp
index 3efce35..a2d5471 100644
--- a/src/dawn/native/Buffer.cpp
+++ b/src/dawn/native/Buffer.cpp
@@ -892,4 +892,16 @@
     return offset == 0 && size == GetSize();
 }
 
+void BufferBase::DumpMemoryStatistics(MemoryDump* dump, const char* prefix) const {
+    // Do not emit for destroyed buffers.
+    if (!IsAlive()) {
+        return;
+    }
+    std::string name = absl::StrFormat("%s/buffer_%p", prefix, static_cast<const void*>(this));
+    dump->AddScalar(name.c_str(), MemoryDump::kNameSize, MemoryDump::kUnitsBytes,
+                    GetAllocatedSize());
+    dump->AddString(name.c_str(), "label", GetLabel());
+    dump->AddString(name.c_str(), "usage", absl::StrFormat("%s", GetUsage()));
+}
+
 }  // namespace dawn::native
diff --git a/src/dawn/native/Buffer.h b/src/dawn/native/Buffer.h
index 6204fff..e87ee17 100644
--- a/src/dawn/native/Buffer.h
+++ b/src/dawn/native/Buffer.h
@@ -114,6 +114,8 @@
     void* GetMappedRange(size_t offset, size_t size, bool writable = true);
     MaybeError Unmap();
 
+    void DumpMemoryStatistics(dawn::native::MemoryDump* dump, const char* prefix) const;
+
     // Dawn API
     void APIMapAsync(wgpu::MapMode mode,
                      size_t offset,
diff --git a/src/dawn/native/DawnNative.cpp b/src/dawn/native/DawnNative.cpp
index 0ba5011..9ac41fa 100644
--- a/src/dawn/native/DawnNative.cpp
+++ b/src/dawn/native/DawnNative.cpp
@@ -42,6 +42,11 @@
 
 namespace dawn::native {
 
+const char MemoryDump::kNameSize[] = "size";
+const char MemoryDump::kNameObjectCount[] = "object_count";
+const char MemoryDump::kUnitsBytes[] = "bytes";
+const char MemoryDump::kUnitsObjects[] = "objects";
+
 const DawnProcTable& GetProcsAutogen();
 
 const DawnProcTable& GetProcs() {
@@ -303,4 +308,8 @@
     return &kFeatureNameAndInfoList[FromAPI(feature)];
 }
 
+void DumpMemoryStatistics(WGPUDevice device, MemoryDump* dump) {
+    FromAPI(device)->DumpMemoryStatistics(dump);
+}
+
 }  // namespace dawn::native
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index 76ab25b..9f7eda1 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -947,6 +947,10 @@
     return &mObjectLists[type];
 }
 
+const ApiObjectList* DeviceBase::GetObjectTrackingList(ObjectType type) const {
+    return &mObjectLists[type];
+}
+
 InstanceBase* DeviceBase::GetInstance() const {
     return mAdapter->GetPhysicalDevice()->GetInstance();
 }
@@ -2335,6 +2339,11 @@
     mToggles.ForceSet(toggle, isEnabled);
 }
 
+void DeviceBase::ForceEnableFeatureForTesting(Feature feature) {
+    mEnabledFeatures.EnableFeature(feature);
+    mFormatTable = BuildFormatTable(this);
+}
+
 void DeviceBase::FlushCallbackTaskQueue() {
     // Callbacks might cause re-entrances. Mutex shouldn't be locked. So we expect there is no
     // locked mutex before entering this method.
@@ -2468,6 +2477,16 @@
     return mMutex == nullptr || mMutex->IsLockedByCurrentThread();
 }
 
+void DeviceBase::DumpMemoryStatistics(dawn::native::MemoryDump* dump) const {
+    std::string prefix = absl::StrFormat("device_%p", static_cast<const void*>(this));
+    GetObjectTrackingList(ObjectType::Texture)->ForEach([&](const ApiObjectBase* texture) {
+        static_cast<const TextureBase*>(texture)->DumpMemoryStatistics(dump, prefix.c_str());
+    });
+    GetObjectTrackingList(ObjectType::Buffer)->ForEach([&](const ApiObjectBase* buffer) {
+        static_cast<const BufferBase*>(buffer)->DumpMemoryStatistics(dump, prefix.c_str());
+    });
+}
+
 IgnoreLazyClearCountScope::IgnoreLazyClearCountScope(DeviceBase* device)
     : mDevice(device), mLazyClearCountForTesting(device->mLazyClearCountForTesting) {}
 
diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h
index 5d2d0d7..c74fe8f 100644
--- a/src/dawn/native/Device.h
+++ b/src/dawn/native/Device.h
@@ -345,6 +345,7 @@
     State GetState() const;
     bool IsLost() const;
     ApiObjectList* GetObjectTrackingList(ObjectType type);
+    const ApiObjectList* GetObjectTrackingList(ObjectType type) const;
 
     std::vector<const char*> GetTogglesUsed() const;
     const tint::wgsl::AllowedFeatures& GetWGSLAllowedFeatures() const;
@@ -446,11 +447,14 @@
         Ref<ComputePipelineBase> computePipeline);
     Ref<RenderPipelineBase> AddOrGetCachedRenderPipeline(Ref<RenderPipelineBase> renderPipeline);
 
+    void DumpMemoryStatistics(dawn::native::MemoryDump* dump) const;
+
   protected:
     // Constructor used only for mocking and testing.
     DeviceBase();
 
     void ForceSetToggleForTesting(Toggle toggle, bool isEnabled);
+    void ForceEnableFeatureForTesting(Feature feature);
 
     MaybeError Initialize(Ref<QueueBase> defaultQueue);
     void DestroyObjects();
diff --git a/src/dawn/native/ObjectBase.cpp b/src/dawn/native/ObjectBase.cpp
index 8d89660..d4a89e3 100644
--- a/src/dawn/native/ObjectBase.cpp
+++ b/src/dawn/native/ObjectBase.cpp
@@ -59,27 +59,23 @@
 }
 
 void ApiObjectList::Track(ApiObjectBase* object) {
-    if (mMarkedDestroyed) {
+    if (mMarkedDestroyed.load(std::memory_order_acquire)) {
         object->DestroyImpl();
         return;
     }
-    std::lock_guard<std::mutex> lock(mMutex);
-    mObjects.Prepend(object);
+    mObjects.Use([&object](auto lockedObjects) { lockedObjects->Prepend(object); });
 }
 
 bool ApiObjectList::Untrack(ApiObjectBase* object) {
-    std::lock_guard<std::mutex> lock(mMutex);
-    return object->RemoveFromList();
+    return mObjects.Use([&object](auto lockedObjects) { return object->RemoveFromList(); });
 }
 
 void ApiObjectList::Destroy() {
     LinkedList<ApiObjectBase> objects;
-    {
-        std::lock_guard<std::mutex> lock(mMutex);
-        mMarkedDestroyed = true;
-        mObjects.MoveInto(&objects);
-    }
-
+    mObjects.Use([&objects, this](auto lockedObjects) {
+        mMarkedDestroyed.store(true, std::memory_order_release);
+        lockedObjects->MoveInto(&objects);
+    });
     while (!objects.empty()) {
         auto* head = objects.head();
         bool removed = head->RemoveFromList();
diff --git a/src/dawn/native/ObjectBase.h b/src/dawn/native/ObjectBase.h
index 6ac6ac9..a2bfbd2 100644
--- a/src/dawn/native/ObjectBase.h
+++ b/src/dawn/native/ObjectBase.h
@@ -32,6 +32,7 @@
 #include <string>
 
 #include "dawn/common/LinkedList.h"
+#include "dawn/common/MutexProtected.h"
 #include "dawn/common/Ref.h"
 #include "dawn/common/RefCounted.h"
 #include "dawn/native/Forward.h"
@@ -98,12 +99,21 @@
     // Destroys and removes all the objects tracked in the list.
     void Destroy();
 
+    template <typename F>
+    void ForEach(F fn) const {
+        mObjects.Use([&fn](const auto lockedObjects) {
+            for (const auto* node = lockedObjects->head(); node != lockedObjects->end();
+                 node = node->next()) {
+                fn(node->value());
+            }
+        });
+    }
+
   private:
     // Boolean used to mark the list so that on subsequent calls to Untrack, we don't need to
-    // reaquire the lock, and Track on new objects immediately destroys them.
-    bool mMarkedDestroyed = false;
-    std::mutex mMutex;
-    LinkedList<ApiObjectBase> mObjects;
+    // reacquire the lock, and Track on new objects immediately destroys them.
+    std::atomic<bool> mMarkedDestroyed{false};
+    MutexProtected<LinkedList<ApiObjectBase>> mObjects;
 };
 
 class ApiObjectBase : public ObjectBase, public LinkNode<ApiObjectBase> {
diff --git a/src/dawn/native/Texture.cpp b/src/dawn/native/Texture.cpp
index b15bbe8..2c6e6e3 100644
--- a/src/dawn/native/Texture.cpp
+++ b/src/dawn/native/Texture.cpp
@@ -1175,6 +1175,41 @@
     return (GetUsage() & wgpu::TextureUsage::TextureBinding) != 0;
 }
 
+void TextureBase::SetSharedResourceMemoryContentsForTesting(
+    Ref<SharedResourceMemoryContents> contents) {
+    mSharedResourceMemoryContents = std::move(contents);
+}
+
+void TextureBase::DumpMemoryStatistics(dawn::native::MemoryDump* dump, const char* prefix) const {
+    // Do not emit for destroyed textures or textures that wrap external shared texture memory.
+    if (!IsAlive() || GetSharedResourceMemoryContents() != nullptr) {
+        return;
+    }
+    std::string name = absl::StrFormat("%s/texture_%p", prefix, static_cast<const void*>(this));
+    dump->AddScalar(name.c_str(), MemoryDump::kNameSize, MemoryDump::kUnitsBytes,
+                    ComputeEstimatedByteSize());
+    dump->AddString(name.c_str(), "label", GetLabel());
+    dump->AddString(name.c_str(), "dimensions", GetSizeLabel());
+    dump->AddString(name.c_str(), "format", absl::StrFormat("%s", GetFormat().format));
+}
+
+uint64_t TextureBase::ComputeEstimatedByteSize() const {
+    DAWN_ASSERT(!IsError());
+    uint64_t byteSize = 0;
+    for (Aspect aspect : IterateEnumMask(SelectFormatAspects(*mFormat, wgpu::TextureAspect::All))) {
+        const AspectInfo& info = mFormat->GetAspectInfo(aspect);
+        for (uint32_t i = 0; i < mMipLevelCount; i++) {
+            Extent3D mipSize = GetMipLevelSingleSubresourcePhysicalSize(i, aspect);
+            byteSize += (mipSize.width / info.block.width) * (mipSize.height / info.block.height) *
+                        info.block.byteSize * mSampleCount;
+        }
+    }
+    if (mDimension == wgpu::TextureDimension::e2D) {
+        byteSize *= mBaseSize.depthOrArrayLayers;
+    }
+    return byteSize;
+}
+
 void TextureBase::APIDestroy() {
     Destroy();
 }
diff --git a/src/dawn/native/Texture.h b/src/dawn/native/Texture.h
index 3180200..ad1f9f9 100644
--- a/src/dawn/native/Texture.h
+++ b/src/dawn/native/Texture.h
@@ -153,6 +153,8 @@
 
     bool IsImplicitMSAARenderTextureViewSupported() const;
 
+    void DumpMemoryStatistics(dawn::native::MemoryDump* dump, const char* prefix) const;
+
     // Dawn API
     TextureViewBase* APICreateView(const TextureViewDescriptor* descriptor = nullptr);
     TextureViewBase* APICreateErrorView(const TextureViewDescriptor* descriptor = nullptr);
@@ -172,6 +174,7 @@
 
     void DestroyImpl() override;
     void AddInternalUsage(wgpu::TextureUsage usage);
+    void SetSharedResourceMemoryContentsForTesting(Ref<SharedResourceMemoryContents> contents);
 
   private:
     struct TextureState {
@@ -187,6 +190,8 @@
 
     std::string GetSizeLabel() const;
 
+    uint64_t ComputeEstimatedByteSize() const;
+
     wgpu::TextureDimension mDimension;
     wgpu::TextureViewDimension
         mCompatibilityTextureBindingViewDimension;  // only used for compatibility mode
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 036ade7..abb1118 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -372,6 +372,7 @@
     "unittests/native/DeviceAsyncTaskTests.cpp",
     "unittests/native/DeviceCreationTests.cpp",
     "unittests/native/LimitsTests.cpp",
+    "unittests/native/MemoryInstrumentationTests.cpp",
     "unittests/native/ObjectContentHasherTests.cpp",
     "unittests/native/StreamTests.cpp",
     "unittests/validation/BindGroupValidationTests.cpp",
diff --git a/src/dawn/tests/unittests/native/MemoryInstrumentationTests.cpp b/src/dawn/tests/unittests/native/MemoryInstrumentationTests.cpp
new file mode 100644
index 0000000..8f32282
--- /dev/null
+++ b/src/dawn/tests/unittests/native/MemoryInstrumentationTests.cpp
@@ -0,0 +1,183 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <string>
+#include <utility>
+
+#include "dawn/native/SharedResourceMemory.h"
+#include "dawn/tests/unittests/native/mocks/BufferMock.h"
+#include "dawn/tests/unittests/native/mocks/DawnMockTest.h"
+#include "dawn/tests/unittests/native/mocks/TextureMock.h"
+
+namespace dawn::native {
+namespace {
+
+using ::testing::ByMove;
+using ::testing::NiceMock;
+using ::testing::Return;
+using ::testing::StrEq;
+
+class MemoryDumpMock : public MemoryDump {
+  public:
+    MemoryDumpMock() {
+        ON_CALL(*this, AddScalar)
+            .WillByDefault(
+                [this](const char* name, const char* key, const char* units, uint64_t value) {
+                    if (key == MemoryDump::kNameSize && units == MemoryDump::kUnitsBytes) {
+                        mTotalSize += value;
+                    }
+                });
+    }
+
+    MOCK_METHOD(void,
+                AddScalar,
+                (const char* name, const char* key, const char* units, uint64_t value),
+                (override));
+
+    void AddString(const char* name, const char* key, const std::string& value) override {}
+
+    uint64_t GetTotalSize() const { return mTotalSize; }
+
+  private:
+    uint64_t mTotalSize = 0;
+};
+
+using MemoryInstrumentationTest = DawnMockTest;
+
+TEST_F(MemoryInstrumentationTest, DumpMemoryStatistics) {
+    MemoryDumpMock memoryDumpMock;
+
+    auto textureLabel = [this](const wgpu::Texture& texture) {
+        return absl::StrFormat("device_%p/texture_%p", device.Get(), texture.Get());
+    };
+
+    auto bufferLabel = [this](const wgpu::Buffer& buffer) {
+        return absl::StrFormat("device_%p/buffer_%p", device.Get(), buffer.Get());
+    };
+
+    // Create a buffer and destroy it and check that its size is not counted.
+    constexpr uint64_t kBufferSize = 31;
+    constexpr wgpu::BufferDescriptor kBufferDesc = {
+        .usage = wgpu::BufferUsage::Uniform,
+        .size = kBufferSize,
+    };
+    wgpu::Buffer destroyedBuffer = device.CreateBuffer(&kBufferDesc);
+    EXPECT_TRUE(destroyedBuffer);
+    destroyedBuffer.Destroy();
+
+    // Create a buffer whose allocated size is larger than requested size and check that allocated
+    // size is counted.
+    constexpr uint64_t kBufferAllocatedSize = 32;
+    Ref<BufferMock> bufferMock = AcquireRef(
+        new NiceMock<BufferMock>(mDeviceMock, FromCppAPI(&kBufferDesc), kBufferAllocatedSize));
+    EXPECT_CALL(*mDeviceMock, CreateBufferImpl).WillOnce(Return(ByMove(std::move(bufferMock))));
+    wgpu::Buffer buffer = device.CreateBuffer(&kBufferDesc);
+
+    EXPECT_CALL(memoryDumpMock, AddScalar(StrEq(bufferLabel(buffer)), MemoryDump::kNameSize,
+                                          MemoryDump::kUnitsBytes, kBufferAllocatedSize));
+
+    // Create a mip-mapped texture and check that all mip level sizes are counted.
+    constexpr wgpu::TextureFormat kRG8UnormTextureFormat = wgpu::TextureFormat::RG8Unorm;
+    const wgpu::TextureDescriptor kMipmappedTextureDesc = {
+        .usage = wgpu::TextureUsage::RenderAttachment,
+        .size = {.width = 30, .height = 20, .depthOrArrayLayers = 10},
+        .format = kRG8UnormTextureFormat,
+        .mipLevelCount = 5,
+        .viewFormatCount = 1,
+        .viewFormats = &kRG8UnormTextureFormat,
+    };
+    wgpu::Texture mipmappedTexture = device.CreateTexture(&kMipmappedTextureDesc);
+
+    // Byte size of entire mip chain =
+    // ((level0 width * level0 height) + ... + (levelN width * levelN height)) * bpp * array layers.
+    constexpr uint64_t kMipmappedTextureSize =
+        (((30 * 20) + (15 * 10) + (7 * 5) + (3 * 2) + (1 * 1)) * 2) * 10;  // 15840
+    EXPECT_CALL(memoryDumpMock,
+                AddScalar(StrEq(textureLabel(mipmappedTexture)), MemoryDump::kNameSize,
+                          MemoryDump::kUnitsBytes, kMipmappedTextureSize));
+
+    // Create a multi-sampled texture and check that sample count is taken into account.
+    const wgpu::TextureDescriptor kMultisampleTextureDesc = {
+        .usage = wgpu::TextureUsage::RenderAttachment,
+        .size = {.width = 30, .height = 20},
+        .format = kRG8UnormTextureFormat,
+        .sampleCount = 4,
+        .viewFormatCount = 1,
+        .viewFormats = &kRG8UnormTextureFormat,
+    };
+    wgpu::Texture multisampleTexture = device.CreateTexture(&kMultisampleTextureDesc);
+    // Expected size = width(30) * height(20) * bytes per pixel(2) * sample count(4).
+    constexpr uint64_t kMultisampleTextureSize = 30 * 20 * 2 * 4;
+    EXPECT_CALL(memoryDumpMock,
+                AddScalar(StrEq(textureLabel(multisampleTexture)), MemoryDump::kNameSize,
+                          MemoryDump::kUnitsBytes, kMultisampleTextureSize));
+
+    // Create a compressed texture and check that counted size is correct.
+    mDeviceMock->ForceEnableFeatureForTesting(Feature::TextureCompressionETC2);
+    constexpr wgpu::TextureFormat kETC2TextureFormat = wgpu::TextureFormat::ETC2RGB8Unorm;
+    const wgpu::TextureDescriptor kETC2TextureDesc = {
+        .usage = wgpu::TextureUsage::CopySrc,
+        .size = {.width = 32, .height = 32},
+        .format = kETC2TextureFormat,
+        .viewFormatCount = 1,
+        .viewFormats = &kETC2TextureFormat,
+    };
+    wgpu::Texture etc2Texture = device.CreateTexture(&kETC2TextureDesc);
+    // Expected size = (width / block width) * (height / block height) * bytes per block.
+    constexpr uint64_t kETC2TextureSize = (32 / 4) * (32 / 4) * 8;
+    EXPECT_CALL(memoryDumpMock, AddScalar(StrEq(textureLabel(etc2Texture)), MemoryDump::kNameSize,
+                                          MemoryDump::kUnitsBytes, kETC2TextureSize));
+
+    // Create a texture and destroy it and check that its size is not counted.
+    wgpu::Texture destroyedTexture = device.CreateTexture(&kMipmappedTextureDesc);
+    EXPECT_TRUE(destroyedTexture);
+    destroyedTexture.Destroy();
+
+    // Create a shared resourc memory texture and check that its size is not counted.
+    constexpr wgpu::TextureFormat kRGBA8UnormTextureFormat = wgpu::TextureFormat::RGBA8Unorm;
+    const wgpu::TextureDescriptor kSharedTextureDesc = {
+        .usage = wgpu::TextureUsage::TextureBinding,
+        .size = {.width = 30, .height = 20},
+        .format = kRGBA8UnormTextureFormat,
+        .viewFormatCount = 1,
+        .viewFormats = &kRGBA8UnormTextureFormat,
+    };
+    Ref<TextureMock> textureMock =
+        AcquireRef(new NiceMock<TextureMock>(mDeviceMock, FromCppAPI(&kSharedTextureDesc)));
+    textureMock->SetSharedResourceMemoryContentsForTesting(
+        AcquireRef(new SharedResourceMemoryContents(nullptr)));
+    EXPECT_CALL(*mDeviceMock, CreateTextureImpl).WillOnce(Return(ByMove(std::move(textureMock))));
+    wgpu::Texture sharedTexture = device.CreateTexture(&kSharedTextureDesc);
+
+    DumpMemoryStatistics(device.Get(), &memoryDumpMock);
+
+    EXPECT_EQ(memoryDumpMock.GetTotalSize(), kBufferAllocatedSize + kMipmappedTextureSize +
+                                                 kMultisampleTextureSize + kETC2TextureSize);
+}
+
+}  // namespace
+}  // namespace dawn::native
diff --git a/src/dawn/tests/unittests/native/mocks/BufferMock.cpp b/src/dawn/tests/unittests/native/mocks/BufferMock.cpp
index c1f646f..9c8d325 100644
--- a/src/dawn/tests/unittests/native/mocks/BufferMock.cpp
+++ b/src/dawn/tests/unittests/native/mocks/BufferMock.cpp
@@ -33,10 +33,13 @@
 
 using ::testing::Return;
 
-BufferMock::BufferMock(DeviceMock* device, const UnpackedPtr<BufferDescriptor>& descriptor)
+BufferMock::BufferMock(DeviceMock* device,
+                       const UnpackedPtr<BufferDescriptor>& descriptor,
+                       std::optional<uint64_t> allocatedSize)
     : BufferBase(device, descriptor) {
-    mBackingData = std::unique_ptr<uint8_t[]>(new uint8_t[GetSize()]);
-    mAllocatedSize = GetSize();
+    mAllocatedSize = allocatedSize.value_or(GetSize());
+    DAWN_ASSERT(mAllocatedSize >= GetSize());
+    mBackingData = std::unique_ptr<uint8_t[]>(new uint8_t[mAllocatedSize]);
 
     ON_CALL(*this, DestroyImpl).WillByDefault([this] { this->BufferBase::DestroyImpl(); });
     ON_CALL(*this, GetMappedPointer).WillByDefault(Return(mBackingData.get()));
@@ -45,8 +48,10 @@
     });
 }
 
-BufferMock::BufferMock(DeviceMock* device, const BufferDescriptor* descriptor)
-    : BufferMock(device, Unpack(descriptor)) {}
+BufferMock::BufferMock(DeviceMock* device,
+                       const BufferDescriptor* descriptor,
+                       std::optional<uint64_t> allocatedSize)
+    : BufferMock(device, Unpack(descriptor), allocatedSize) {}
 
 BufferMock::~BufferMock() = default;
 
diff --git a/src/dawn/tests/unittests/native/mocks/BufferMock.h b/src/dawn/tests/unittests/native/mocks/BufferMock.h
index 60bcf69..7c6740d 100644
--- a/src/dawn/tests/unittests/native/mocks/BufferMock.h
+++ b/src/dawn/tests/unittests/native/mocks/BufferMock.h
@@ -39,8 +39,12 @@
 
 class BufferMock : public BufferBase {
   public:
-    BufferMock(DeviceMock* device, const UnpackedPtr<BufferDescriptor>& descriptor);
-    BufferMock(DeviceMock* device, const BufferDescriptor* descriptor);
+    BufferMock(DeviceMock* device,
+               const UnpackedPtr<BufferDescriptor>& descriptor,
+               std::optional<uint64_t> allocatedSize = std::nullopt);
+    BufferMock(DeviceMock* device,
+               const BufferDescriptor* descriptor,
+               std::optional<uint64_t> allocatedSize = std::nullopt);
     ~BufferMock() override;
 
     MOCK_METHOD(void, DestroyImpl, (), (override));
diff --git a/src/dawn/tests/unittests/native/mocks/DeviceMock.h b/src/dawn/tests/unittests/native/mocks/DeviceMock.h
index 6a6a50c..87c41ee 100644
--- a/src/dawn/tests/unittests/native/mocks/DeviceMock.h
+++ b/src/dawn/tests/unittests/native/mocks/DeviceMock.h
@@ -45,6 +45,7 @@
   public:
     // Exposes some protected functions for testing purposes.
     using DeviceBase::DestroyObjects;
+    using DeviceBase::ForceEnableFeatureForTesting;
     using DeviceBase::ForceSetToggleForTesting;
 
     // TODO(lokokung): Use real DeviceBase constructor instead of mock specific one.
diff --git a/src/dawn/tests/unittests/native/mocks/TextureMock.h b/src/dawn/tests/unittests/native/mocks/TextureMock.h
index e85c129..5592357 100644
--- a/src/dawn/tests/unittests/native/mocks/TextureMock.h
+++ b/src/dawn/tests/unittests/native/mocks/TextureMock.h
@@ -41,6 +41,8 @@
     TextureMock(DeviceMock* device, const TextureDescriptor* descriptor);
     ~TextureMock() override;
 
+    using TextureBase::SetSharedResourceMemoryContentsForTesting;
+
     MOCK_METHOD(void, DestroyImpl, (), (override));
 };