返回「计算机、信息技术与工程」

WebGL 应用与微信小游戏运行时架构、内存组成和分析方法

WebGL 应用与微信小游戏运行时架构、内存组成和分析方法 只讨论 浏览器 WebGL 应用、Unity WebGL、Emscripten/WebAssembly、微信小游戏运行时,以及它们在内存快照和 Android 真机内存分析中的表现 。其中部分示例来自本会话里的 heap snapshot 结构,例如 HEAPU8 ArrayBuffer 、 FS nameTable contents Uint8Array ArrayBu…

更多
Markdown 结构化数据
本文目录 81 个章节

WebGL 应用与微信小游戏运行时架构、内存组成和分析方法

只讨论 浏览器 WebGL 应用、Unity WebGL、Emscripten/WebAssembly、微信小游戏运行时,以及它们在内存快照和 Android 真机内存分析中的表现。其中部分示例来自本会话里的 heap snapshot 结构,例如 HEAPU8 -> ArrayBufferFS -> nameTable -> contents -> Uint8Array -> ArrayBuffer 这类 retainer path,用作通用解读样例。


1. WebGL 应用的整体分层架构

一个现代 WebGL 游戏或图形应用,通常不是“JavaScript 调用 GPU”这么简单,而是由多层运行时组成。理解内存时,必须先分清每一层。

1.1 从下到上的典型架构

硬件层
  CPU / RAM / GPU / 显存 / 显示系统
    ↓
操作系统层
  Android / iOS / Windows / macOS
  进程管理、内存页、图形驱动、文件系统、网络
    ↓
浏览器或宿主运行时
  Chrome / Safari / Firefox / Edge
  或微信小游戏宿主、XWeb、小游戏 JS Runtime
    ↓
JavaScript 引擎
  V8 / JavaScriptCore / QuickJS-like runtime / 宿主自研封装
  负责 JS 对象、闭包、字符串、ArrayBuffer、GC、JIT/Bytecode
    ↓
WebAssembly 引擎
  加载 .wasm
  编译 Wasm code
  管理 WebAssembly.Memory、Table、Instance、Module
    ↓
WebGL 实现层
  WebGLRenderingContext / WebGL2RenderingContext
  浏览器或宿主把 WebGL API 转成 OpenGL ES / Metal / Vulkan / Direct3D / ANGLE / 原生图形 API
    ↓
Canvas 层
  HTMLCanvasElement 或小游戏 Canvas
  作为 WebGL 绘制目标
    ↓
Emscripten Runtime
  Module、HEAPU8、FS、dynCall、wasm exports、JS glue code
    ↓
游戏引擎运行时
  Unity WebGL / Cocos / Laya / PlayCanvas / Three.js / Babylon.js
    ↓
游戏业务层
  C# / TypeScript / JavaScript / Lua / C++ 逻辑
  场景、资源、UI、网络、插件、监控、截图、音频

每一层都可能分配内存,而且这些内存不一定都能在 JavaScript heap snapshot 里看到。


2. 浏览器 WebGL 应用的核心组成

2.1 JavaScript 应用层

普通 WebGL 应用的上层通常是 JavaScript 或 TypeScript。它负责:

1. 创建 Canvas;
2. 获取 WebGL context;
3. 加载资源;
4. 管理渲染循环;
5. 创建 shader / texture / buffer / framebuffer;
6. 处理输入、网络、音频、UI;
7. 与 Wasm 或引擎 runtime 通信。

常见代码形态:

const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");

const tex = gl.createTexture();
const fbo = gl.createFramebuffer();
const buffer = gl.createBuffer();

这类 JS 对象本身通常很小,但它们对应的底层 native / GPU 资源可能很大。


2.2 WebGL API 层

WebGL 是暴露给 JavaScript 的图形 API。它的对象通常包括:

WebGLTexture        纹理对象
WebGLBuffer         顶点 / 索引 / uniform buffer
WebGLFramebuffer    离屏渲染目标
WebGLRenderbuffer   深度 / 模板 / 多采样 renderbuffer
WebGLShader         顶点 / 片元 shader
WebGLProgram        shader program
WebGLQuery          GPU query
WebGLSync           GPU 同步对象

例如 framebufferTexture2D() 会把一个 texture 附加到 framebuffer 上,作为 framebuffer 的颜色、深度或模板附件。(MDN Web Docs)

内存理解重点:

JS heap 里看到的 WebGLTexture 对象:
  只是一个 JS wrapper。

真正的大内存:
  可能在 GPU 显存、图形驱动、浏览器 renderer native memory、Android Graphics / GL mtrack 中。

所以,WebGL 纹理泄露、FBO 泄露、PBO 泄露,经常 不会 在 JS heap snapshot 中表现为大对象,但会在 Android dumpsys meminfoGraphicsGL mtrackEGL mtrackPrivate OtherRssAnon 等指标里表现出来。


2.3 WebAssembly 层

WebAssembly 是浏览器中运行 C/C++/Rust/C# 转译代码的低层执行格式。Unity WebGL、部分 Cocos 模块、物理引擎、音视频编码器、图像编码器都可能使用 Wasm。

WebAssembly 应用通常包含:

.wasm 文件
  编译后的二进制代码。

WebAssembly.Module
  解析后的 Wasm 模块。

WebAssembly.Instance
  实例化后的运行实例。

WebAssembly.Memory
  Wasm 线性内存。

WebAssembly.Table
  函数表,常用于间接调用、函数指针、dynCall。

在 heap snapshot 中,常见表现包括:

Module
Table
WasmDispatchTable
WasmTrustedInstanceData
Managed<wasm::NativeModule>
WebAssembly.Memory.buffer
ArrayBuffer
system / JSArrayBufferData

Managed<wasm::NativeModule> 通常表示 V8 / JS 引擎保存的 Wasm 编译产物、元数据或 native module 结构。它不是业务 JS 对象,也不一定能由业务代码直接释放。


3. Unity WebGL 的架构

Unity WebGL 是理解小游戏 WebGL 内存的核心场景之一。

3.1 Unity WebGL 的编译链路

Unity WebGL 的大致编译链路是:

C# 游戏代码
  ↓ IL2CPP
C++ 代码
  ↓ Emscripten
WebAssembly

Unity 引擎 C/C++ runtime
  ↓ Emscripten
WebAssembly

Unity WebGL loader / framework JS
  ↓
浏览器或小游戏宿主

Unity 文档说明,Unity WebGL 使用 Emscripten 将 Unity runtime 的 C/C++ 代码交叉编译成 WebAssembly;C# 脚本则通过 IL2CPP 转成 C++,再由 Emscripten 编译成 Wasm。(Unity Documentation)


3.2 Unity WebGL 构建产物

Unity WebGL 构建后通常会产生:

index.html
  浏览器入口页面。

.loader.js
  加载器脚本。

.framework.js
  Unity WebGL JavaScript framework / glue code。

.wasm
  Unity runtime + IL2CPP 代码编译后的 Wasm。

.data / .data.unityweb / data.unity3d
  资源数据包,包含场景、资源、序列化数据等。

StreamingAssets / AssetBundles / Addressables
  运行时资源。

Unity 官方文档也说明 Web Build folder 会包含运行 Web 应用所需的文件。(Unity Documentation)


3.3 Unity Heap / Wasm Heap

Unity WebGL 有一个非常重要的内存概念:Unity Heap

Unity 文档说明,Unity heap 是一个连续内存块,用于保存 Unity runtime objects,包括 managed/native objects、assets、scenes、shaders 等;这个 heap 会以 WebAssembly Memory 的形式存在,其 buffer 是一个可调整大小的 ArrayBuffer。(Unity Documentation)

在 heap snapshot 中,它通常表现为:

Module
  -> HEAPU8
    -> Uint8Array
      -> ArrayBuffer
        -> system / JSArrayBufferData

示例 retainer path:

ArrayBuffer retained=256MB
  <- Uint8Array
  <- HEAPU8
  <- Module

本会话的快照示例中也出现了 ArrayBuffer 256MB -> HEAPU8 这种典型结构。

专业解释:

HEAPU8 不是一块新内存。
HEAPU8 是 WebAssembly.Memory.buffer 的 Uint8Array 视图。
真正的大内存是 backing store,也就是 system / JSArrayBufferData。

4. Emscripten Runtime 的核心概念

Emscripten 是把 C/C++ 编译到 WebAssembly 的工具链。Unity WebGL、很多 C++ 游戏引擎、图像编码器、物理库都会间接使用它。

4.1 Module

Module 是 Emscripten JavaScript glue code 的核心对象。它通常保存:

Module.HEAP8
Module.HEAPU8
Module.HEAP16
Module.HEAPU16
Module.HEAP32
Module.HEAPU32
Module.HEAPF32
Module.HEAPF64

Module._malloc
Module._free
Module.ccall
Module.cwrap
Module.FS
Module.canvas
Module.asm
Module.wasmMemory

Module 本身不一定很大,但它能引用很多大对象。例如:

Module.HEAPU8.buffer
Module.FS.nameTable
Module.canvas
Module.asm.__indirect_function_table

所以 heap snapshot 中看到 Module 作为 retainer 时,不能直接说“Module 分配了这些内存”,更准确的话术是:

Module 是 Emscripten runtime 的根对象,它让 Wasm heap、FS、函数表等结构保持可达。


4.2 HEAPU8 / HEAP32 / HEAPF32

这些是对同一个 WebAssembly.Memory.buffer 的不同 typed array 视图。

HEAPU8   Uint8Array,按字节访问 Wasm 内存
HEAP8    Int8Array
HEAPU16  Uint16Array
HEAP32   Int32Array
HEAPF32  Float32Array
HEAPF64  Float64Array

它们都不是独立的大内存。真正大的是:

WebAssembly.Memory.buffer
  -> ArrayBuffer
    -> native: system / JSArrayBufferData

4.3 Wasm 线性内存

Wasm 线性内存可以理解为 C/C++ 程序看到的一整块连续地址空间。C++ 里的:

malloc()
new
std::vector
std::string
Unity native objects
IL2CPP managed heap
decoded asset data
temporary buffers

最终很多都会落在这块线性内存里。

关键特征:

1. Wasm heap 增长后通常不会自动缩回。
2. C++ 对象释放后,只是 allocator 内部可复用,不代表 WebAssembly.Memory.buffer 变小。
3. JavaScript heap snapshot 能看到 Wasm Memory 的 ArrayBuffer 容量,但不一定能知道里面哪些 C++ 对象占用。
4. 如果 heap snapshot 前后 HEAPU8.buffer.byteLength 没变,不代表 C++ 内部没有分配变化;需要看 sbrk、malloc 统计或引擎内部内存统计。

4.4 Emscripten FS:MEMFS / IDBFS / WasmFS

Emscripten 提供一个虚拟文件系统,使 C/C++ 代码可以使用类似 POSIX 的文件 API。

常见文件系统:

MEMFS
  内存文件系统。
  文件内容保存在 JS/Wasm 内存中。
  默认经常挂载在 /。

IDBFS
  IndexedDB-backed 文件系统。
  通常先在内存中维护文件,再与 IndexedDB 同步。

NODEFS
  Node.js 环境下映射本地文件系统。

WasmFS
  新的 Wasm-based 文件系统层。

Emscripten 文档说明,MEMFS 会在 runtime 初始化时挂载到 /;预加载文件可以在启动时放进 MEMFS。(Emscripten) Emscripten 也有专门的 File System API 文档,并说明 WasmFS 是新的高性能、多线程文件系统层。(Emscripten)

在 heap snapshot 中,MEMFS 文件常见 retainer path 是:

FS
  -> nameTable
    -> FSNode
      -> contents
        -> Uint8Array
          -> ArrayBuffer
            -> system / JSArrayBufferData

本会话的快照示例中就出现了 FS -> nameTable -> contents -> Uint8Array -> ArrayBuffer 43MB 的结构。

专业解释:

这条链说明“大内存是一个 Emscripten FS 文件的内容”。 它不能单独证明这个文件是谁写入的,需要结合文件路径、内容头、日志或 FS dump 才能归因。


5. 微信小游戏与浏览器 WebGL 应用的区别

微信小游戏可以运行 WebGL 和 Wasm,但它不是标准浏览器页面。

5.1 运行环境差异

标准浏览器 WebGL 应用通常运行在:

HTML 页面
DOM
Canvas
浏览器网络 API
浏览器存储 API
标准 DevTools

微信小游戏运行在:

微信宿主 App
小游戏 JS runtime
微信封装的 Canvas / WebGL API
wx API
小游戏文件系统
小游戏网络 API
小游戏包体 / 分包 / 资源缓存机制
微信开发者工具 / 真机远程调试

Cocos Creator 文档对微信小游戏的描述很适合作为通用理解:微信小游戏运行环境是微信小程序环境的扩展,在小程序环境基础上提供了 WebGL 接口封装,但这些接口由微信团队通过原生实现封装,不能等同于浏览器环境。(Cocos Creator)

因此,同样一段 WebGL 或 Unity WebGL 代码:

在 Chrome 浏览器里:
  内存可能表现为 renderer process + GPU process。

在微信小游戏里:
  内存可能表现为 com.tencent.mm:appbrandX、tools、xweb、GPU/native mtrack、Private Other 等多个进程或类别。

5.2 Canvas / WebGL 差异

浏览器中:

const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");

小游戏中通常是:

const canvas = wx.createCanvas();
const gl = canvas.getContext("webgl");

差异包括:

1. 没有完整 DOM。
2. Canvas 由小游戏宿主创建和管理。
3. WebGL API 由宿主封装,底层可能走微信/XWeb/平台图形实现。
4. 部分扩展、像素读取、canvas 导出、文件转换行为可能和浏览器不同。
5. 真机内存归因通常更依赖 Android/iOS 系统工具,而不是单靠 JS heap snapshot。

5.3 文件系统差异

标准浏览器 WebGL:

Emscripten MEMFS / IDBFS
Browser Cache
IndexedDB
Fetch / XHR response cache

微信小游戏:

小游戏包体
分包
远程资源
微信本地缓存目录
小游戏文件系统 API
Unity / Emscripten 虚拟 FS
宿主内部资源缓存

在小游戏里,Emscripten FS、Unity datapackage、小游戏宿主缓存、网络下载缓存可能同时存在。它们在内存中可能表现为:

JS ArrayBuffer
Uint8Array
FSNode.contents
Native file buffer
Network response buffer
Decompression buffer
小游戏宿主内部 cache

5.4 Unity WebGL 转小游戏的额外成本

Unity / 团结引擎的小游戏 WebGL 平台文档指出,WebGL 运行机制和文件系统让内存结构与原生 App 很不一样,通常会多出 Wasm 加载与编译、Wasm Heap 未使用部分、JS File System 等内存;文档还特别提示,Emscripten 使用 JS + IndexedDB 模拟文件系统时,JS 中可能始终存有文件 copy,因此在微信小游戏中应尽量避免使用 Emscripten 文件系统。(Unity User Manual)

这对内存分析很重要:

小游戏 WebGL 内存高,不一定是业务泄露。
它可能是:
  Wasm 文件加载和编译成本;
  Unity Heap 预留容量;
  data package / FS 文件内容;
  浏览器/宿主 JIT 和 code cache;
  GPU/Canvas/WebGL native 资源;
  资源下载、解压、缓存;
  多进程 PSS 统计口径导致的波动。

6. WebGL / 小游戏应用的内存组成

从内存分析角度,可以把 WebGL 应用内存分成以下几类。


6.1 JavaScript Heap

包括:

普通 Object
Array
Map / Set / WeakMap
Closure
Function
Promise
字符串
TypedArray wrapper
ArrayBuffer wrapper
事件回调
定时器回调
模块对象
小游戏 runtime JS 对象
引擎 JS glue code

在 heap snapshot 中常见类型:

object
array
string
concatenated string
closure
code
object shape / system Map
system / Context

注意:

ArrayBuffer wrapper 很小。
真正的二进制 backing store 通常体现在 native: system / JSArrayBufferData。

6.2 ArrayBuffer / TypedArray backing store

典型对象链:

Uint8Array
  -> buffer
    -> ArrayBuffer
      -> backing_store
        -> system / JSArrayBufferData

常见来源:

1. Wasm Memory buffer;
2. 网络 response ArrayBuffer;
3. 文件读取结果;
4. 图片/音频/压缩包二进制;
5. WebGL readPixels 结果;
6. Emscripten FS 文件 contents;
7. 上传 request body;
8. 解压缩中间 buffer;
9. 编码器输入 / 输出 buffer。

在 heap snapshot 中,如果看到:

native | system / JSArrayBufferData | 43MB

不要停在这个节点,要往 retainer path 往上看:

是 HEAPU8 持有?
是 FSNode.contents 持有?
是 Response / Promise 持有?
是 Upload queue 持有?
是 WebGL readback buffer 持有?

本会话快照示例中 system / JSArrayBufferData 排名前两项是 256MB 和 43MB,说明大头是 ArrayBuffer backing store,而不是普通 JS 对象。


6.3 Wasm Linear Memory / Unity Heap

表现:

Module.HEAPU8.buffer
ArrayBuffer
system / JSArrayBufferData

常见大小:

64MB
128MB
192MB
256MB
512MB
更高

取决于:

INITIAL_MEMORY
ALLOW_MEMORY_GROWTH
MAXIMUM_MEMORY
Unity Player Settings
运行时峰值分配
资源规模
C++ allocator 行为

分析重点:

HEAPU8.buffer.byteLength:
  表示 Wasm heap 容量。

C++ 已使用内存:
  需要看 malloc/sbrk/Unity profiler/引擎统计。

释放 C++ 对象:
  不一定让 HEAPU8.buffer 变小。

Wasm heap 增长:
  一旦扩容,通常成为新的常驻高水位。

6.4 Wasm 编译产物和引擎内部结构

heap snapshot 中可能看到:

Managed<wasm::NativeModule>
WasmTrustedInstanceData
WasmDispatchTable
WasmImportData
Table
Code
BytecodeArray
FeedbackCell
FeedbackVector
SharedFunctionInfo

专业解释:

Managed<wasm::NativeModule>
  Wasm 编译后的 native module / 引擎内部结构。

WasmDispatchTable / Table
  Wasm 函数表、间接调用表、函数指针相关结构。

BytecodeArray / FeedbackVector / Code
  JavaScript 引擎为 JS 函数生成的字节码、反馈向量、优化信息。

WAGame.js / framework.js / plugin.js string
  运行时代码或源码字符串,不一定是泄露。

本会话快照示例中存在 Managed<wasm::NativeModule> 35.23MBWasmTrustedInstanceDataWasmDispatchTableTable 等,这类对象通常属于 Wasm/JS 引擎运行时成本。


6.5 Emscripten FS / Unity datapackage / 资源包

典型 retainer path:

FS
  -> nameTable
    -> Array[index]
      -> FSNode
        -> contents
          -> Uint8Array
            -> ArrayBuffer
              -> JSArrayBufferData

可能来源:

1. Unity data package;
2. 首包资源;
3. 预加载文件;
4. 运行时写入的临时文件;
5. 下载缓存;
6. 截图临时文件;
7. 日志文件;
8. AssetBundle / Addressables cache。

重要判断:

FS -> nameTable -> contents 只能证明“这是 FS 文件内容”。
不能直接证明“这是哪个模块写入的”。
必须进一步看:
  文件路径;
  文件头;
  FS dump;
  写入调用栈;
  运行日志;
  阶段差分。

如果文件头是 UnityWebData1.0,通常更像 Unity datapackage,而不是业务临时缓存。


6.6 WebGL / GPU / Graphics 资源内存

常见资源:

Texture
Renderbuffer
Framebuffer
Vertex Buffer
Index Buffer
Uniform Buffer
Shader
Program
PBO / Pixel Buffer Object
VAO
Transform feedback

这些资源大多不在 JS heap 中以真实大小体现。

典型内存估算:

RGBA8 texture:
  width × height × 4 bytes

RGBA8 texture with mipmaps:
  width × height × 4 × 1.33 左右

Depth24/Stencil8:
  width × height × 4 bytes

双缓冲 / 多缓冲:
  单份大小 × buffer count

PBO ring:
  requiredBytes × ringSize

例如:

1280 × 720 × 4 ≈ 3.52 MB
1920 × 1080 × 4 ≈ 7.91 MB
2560 × 1440 × 4 ≈ 14.06 MB
三份 2560×1440 RGBA ≈ 42.19 MB

所以“截图、离屏渲染、readPixels、PBO ring、canvas 导出”很容易产生 20–50MB 级别的波动,但不一定在 JS heap snapshot 中出现。


6.7 Canvas backing store

Canvas 本身也有 backing store。实际像素尺寸通常是:

cssWidth × devicePixelRatio
cssHeight × devicePixelRatio

例如:

CSS 1280×720
devicePixelRatio = 2
实际 backing store = 2560×1440
RGBA = 2560×1440×4 ≈ 14.06 MB

如果同时有:

主 Canvas
离屏 Canvas
截图 Canvas
临时 Canvas
WebGL framebuffer
导出图片 buffer

内存会快速增加。


6.8 网络、解压和上传内存

常见链路:

fetch / wx.request
  -> response ArrayBuffer
  -> 解压 buffer
  -> 解析 buffer
  -> 写入 FS 或 Wasm heap
  -> 上传 body copy

常见重复副本:

网络 response buffer
解压前 compressed buffer
解压后 uncompressed buffer
Wasm heap copy
FS contents copy
业务 cache copy
上传 request body copy
宿主内部 native copy

优化目标不是“完全没有 buffer”,而是减少同一份数据在同一时间窗口内出现多份副本。


6.9 图片、音频、字体和视频

图片资源可能经历:

compressed image file
decoded RGBA pixels
GPU texture
mipmap chain
CPU-side cache

音频资源可能经历:

compressed audio file
decoded PCM
audio engine buffer
streaming buffer
native audio cache

字体资源可能经历:

font file
glyph cache
texture atlas
layout cache

这些通常也不完全体现在 JS heap snapshot 中。


7. Chrome / 微信开发者工具 Heap Snapshot 术语

Chrome DevTools 文档把 heap snapshot 用于分析对象分布和内存泄露;Memory 面板里常见 Shallow SizeRetained Size 两个概念。(Chrome for Developers)

7.1 Shallow Size / Self Size

含义:

对象自身占用的内存。

例如:

ArrayBuffer wrapper self_size = 52 B
Uint8Array wrapper self_size = 60 B

这不代表 buffer 只有几十字节。真正数据可能在 backing store。


7.2 Retained Size

含义:

如果这个对象被释放,并且它支配的对象也变为不可达,总共可以释放的内存。

Retained Size 适合找 dominator,但要小心:

某个 Object retained 很大,可能只是因为它是一个根容器。
它不一定是分配源。

例如:

FS object retained 43MB

专业解释不是“FS 对象自己有 43MB”,而是:

FS 通过 nameTable 支配了某个 FSNode.contents,而 contents 持有 43MB ArrayBuffer。


7.3 Distance

含义:

对象到 GC Root 的最短引用距离。

距离越小,越接近根对象。常见根:

window / globalThis
GameGlobal
Module
GC roots
Global handles
Native contexts
Internalized strings

距离小不代表泄露,只说明它容易长期存活。


7.4 Retainer Path

Retainer path 是:

谁持有了这个对象?
这个对象为什么还活着?

它不是 allocation stack。

正确表述:

Retainer path 证明“当前由谁保活”。
Allocation stack 才证明“当初由谁分配”。

例如:

FS -> nameTable -> contents -> Uint8Array -> ArrayBuffer

只能证明:

这个 ArrayBuffer 当前作为一个 FS 文件内容存在。

不能单凭这条链判断:

是谁写了这个文件?
这个文件是什么?
是否是泄露?

7.5 常见 node type 解读

type 含义 常见来源
object JS 对象 Object、ArrayBuffer、Uint8Array、Module、FS
array JS 数组或内部数组 object elements、nameTable、属性数组
string 字符串 源码字符串、日志、JSON、base64
code JS 引擎代码对象 BytecodeArray、Code、SharedFunctionInfo
closure 函数闭包 回调、模块函数、事件监听
native JS 引擎外部/native 对象 JSArrayBufferData
hidden 引擎内部对象 WasmNativeModule、Map、Context
synthetic 快照合成根节点 GC roots、Global handles

8. 典型快照结构的专业解读

8.1 system / JSArrayBufferData

看到:

native: system / JSArrayBufferData 256MB

专业解读:

这是某个 ArrayBuffer 的外部 backing store。
它是字节数据真正占用的地方。
需要往上找 backing_store 的 owner ArrayBuffer。

继续看 retainer:

JSArrayBufferData
  <- backing_store
ArrayBuffer
  <- buffer
Uint8Array
  <- HEAPU8
Module

结论:

这是 Wasm linear memory / Unity Heap。

如果 retainer 是:

JSArrayBufferData
  <- ArrayBuffer
  <- Uint8Array
  <- contents
FSNode
  <- nameTable
FS

结论:

这是 Emscripten FS 文件内容。

8.2 Module -> HEAPU8 -> ArrayBuffer

专业含义:

Module 是 Emscripten runtime 全局对象。
HEAPU8 是 Wasm memory 的 Uint8Array 视图。
ArrayBuffer 是 WebAssembly.Memory.buffer。
JSArrayBufferData 是 backing store。

不要说:

Module 分配了 256MB。

应该说:

Module 持有 Wasm linear memory 的视图,使这块 Wasm heap 在 retainer path 中可见。

8.3 FS -> nameTable -> contents -> Uint8Array

专业含义:

Emscripten FS 中某个文件节点持有文件内容。

可能是:

Unity data package
预加载资源
临时文件
缓存文件
日志文件
截图文件
下载文件

排查方式:

1. dump FS 文件路径;
2. 打印文件 size;
3. 读取文件头;
4. hook FS.writeFile / FS.write;
5. 结合日志和时间点。

8.4 Managed<wasm::NativeModule>

专业含义:

JS 引擎中与 Wasm module 编译结果相关的 native 内部结构。

常见归属:

Wasm 代码编译产物
Wasm 函数元数据
JIT / baseline / optimized code
引擎内部缓存

通常不能通过业务 JS 直接释放。减少它的方式通常是:

减小 Wasm 文件体积;
裁剪未用代码;
按需加载模块;
Wasm 分包;
避免一次性加载全部功能。

8.5 BytecodeArray / FeedbackVector / SharedFunctionInfo

专业含义:

JavaScript 引擎为 JS 代码生成的执行数据。

常见来源:

Unity framework.js
小游戏 runtime JS
引擎 glue code
业务 bundle
插件 bundle
Babel runtime
polyfill

这类对象在 heap snapshot 里可能很多,但一般不是 20–50MB 驻留增长的主因,除非出现大量动态生成代码、eval、new Function 或反复加载 bundle。


8.6 string 大对象

常见来源:

源码字符串
内联 source map
base64 图片
base64 音频
JSON payload
日志缓存
错误堆栈
压缩包文本

专业判断:

源码字符串:
  多数是运行时或打包成本,不一定是泄露。

base64:
  高风险,因为体积比二进制更大,并且字符串在 JS heap 里很明显。

JSON:
  如果长期缓存大型上报包或配置包,可能泄露。

日志:
  如果数组无上限,会持续增长。

9. Android 真机内存指标

在微信小游戏真机排查中,JS heap snapshot 只覆盖一部分内存。Android 进程内存更适合用:

adb shell dumpsys meminfo <pid>
/proc/<pid>/smaps_rollup
/proc/<pid>/status
showmap
Perfetto
heapprofd
GPU/mtrack 指标

Android 官方文档说明 dumpsys meminfo 可用于查看进程内存,且通常关注 Pss TotalPrivate DirtyPrivate Dirty 很重要,因为它是进程私有且不能轻易回收的脏页。(Android Developers) Perfetto 文档也强调共享内存会映射到多个进程,PSS 会按比例分摊,因此分析时经常要关注 Private Dirty 来避免共享页干扰。(Android Git Repositories)

9.1 RSS

Resident Set Size
当前实际驻留在物理内存中的页总量。

特点:

包含共享页。
容易受系统缓存、共享库、其他进程影响。
不能直接等同于“这个进程独占了多少内存”。

9.2 PSS

Proportional Set Size
按比例分摊共享页后的内存。

例如某个 100MB 共享库被 4 个进程共享,每个进程 PSS 可能只算 25MB。

优点:

适合估算进程对系统总内存的贡献。

缺点:

共享页归属变化会造成波动。
跨运行比较时容易不稳定。

9.3 Private Dirty

进程私有、已修改、不能直接丢弃的内存页。

这是判断真实驻留增长的重要指标。

如果某阶段:

Private Dirty +30MB
RssAnon +30MB
持续 60 秒不回落

这比单纯 PSS +30MB 更能证明有真实私有内存增长。


9.4 RssAnon / RssFile / RssShmem

来自 /proc/<pid>/status

RssAnon
  匿名内存,通常对应 native heap、Wasm memory、malloc、匿名 mmap。

RssFile
  file-backed mapping,例如 so、dex、wasm 文件、资源文件映射。

RssShmem
  共享内存,例如图形 buffer、ashmem、共享映射。

判断:

RssAnon 增长:
  更像 malloc / Wasm heap / native heap。

RssFile 增长:
  更像文件映射、代码、资源、cache。

RssShmem / Graphics 增长:
  更像图形缓冲、Canvas、GPU、Surface。

9.5 dumpsys meminfo 常见分类

分类 通常含义
Java Heap Java/Kotlin 对象,微信宿主或插件 Java 层
Native Heap C/C++ malloc/new
Code so、dex、oat、jit code、wasm/code mapping
Stack 线程栈
Graphics 图形相关内存
GL mtrack / EGL mtrack OpenGL/EGL 资源
Private Other 不容易归类的 native/private memory,WebGL/XWeb/宿主内部常见
System 系统归属或分摊内存

微信小游戏中,Private Other 增长并不罕见,因为 WebGL、XWeb、Canvas、宿主桥接、资源缓存都可能落在这里。


10. 为什么“启动前 700MB,另一次启动前 800MB”很常见

WebGL 小游戏内存基线波动大,常见原因:

1. 微信多进程状态不同;
2. appbrand 进程复用;
3. 上一次小游戏残留缓存;
4. XWeb / JS 引擎 code cache;
5. Wasm 编译缓存;
6. 资源缓存;
7. GPU / Canvas / Surface 状态不同;
8. Android 系统内存压力不同;
9. PSS 共享页分摊变化;
10. 是否已加载 Unity datapackage;
11. 是否已触发音频、字体、图片、网络、WebGL context。

因此,专业测试口径应是:

同一进程、同一次运行、同一阶段、稳定后采样。

不要比较:

Run A 初始 700MB
Run B 初始 800MB

而要比较:

同一个 PID:
  T0: WebGL/Unity 加载完成,目标模块未启动
  T1: 目标模块启动后
  T2: 首次功能触发后
  T3: 清理后

11. 内存增长、驻留、泄露的专业定义

11.1 峰值内存

某个操作期间短时间增加,随后回落。

例如:

下载资源时 response buffer + 解压 buffer 同时存在。
截图时 readPixels buffer + encode buffer 同时存在。
上传时 body copy + native copy 同时存在。

峰值问题会导致 OOM,但不是泄露。


11.2 驻留内存

操作完成后仍然保留,并且长期稳定存在。

例如:

初始化后的 WebGL texture cache
Wasm heap 扩容后的容量
引擎单例缓存
常驻资源
全局 buffer pool

驻留不一定是 bug,但需要判断是否必要、是否过早、是否过大。


11.3 泄露

重复执行同一操作后,稳定基线持续上升,且没有释放路径。

例如:

每次截图后多一个未 delete 的 texture;
每次请求后 pending map 少删一个 entry;
每次 start 后多注册一组事件监听;
每次写 FS 临时文件后未 unlink;
每次 push 日志无上限。

证明泄露要看趋势,不看单次。


12. WebGL 应用内存分析方法

12.1 分阶段基线法

推荐阶段:

T0: 宿主进程已稳定,游戏未加载
T1: WebGL/Unity runtime 加载完成
T2: data package / 首包资源加载完成
T3: 主场景进入,静置 60 秒
T4: 目标功能初始化前
T5: 目标功能初始化后
T6: 首次功能调用后
T7: 功能完成后静置 60 秒
T8: stop / destroy / cleanup 后
T9: 重复 N 次后

每个阶段采集:

JS heap snapshot
HEAPU8.buffer.byteLength
FS 大文件列表
WebGL 资源统计
dumpsys meminfo
smaps_rollup
log marker

12.2 JS Heap Snapshot 分析步骤

步骤:

1. 按 retained size 排序;
2. 找大对象;
3. 区分 wrapper 和 backing store;
4. 展开 retainer path;
5. 判断对象类别:
   Wasm heap?
   FS file?
   network buffer?
   WebGL readback?
   string/base64?
   closure queue?
6. 做前后 snapshot diff;
7. 对无法归因的大 buffer 加分配栈或 tag。

重点:

Retainer path 回答“为什么还活着”。
Allocation instrumentation 回答“哪里分配的”。
Snapshot diff 回答“阶段之间新增了什么”。

12.3 Android 真机分析步骤

推荐命令:

adb shell "ps -A | grep -i com.tencent.mm"

找到小游戏进程,例如:

com.tencent.mm:appbrand0
com.tencent.mm:appbrand1
com.tencent.mm:tools
com.tencent.mm:toolsmp

然后采样:

adb shell "PID=<pid>; while true; do \
echo '===== MEM '$(date +%s)' pid='$PID' ====='; \
dumpsys meminfo $PID | sed -n '1,120p'; \
echo '----- smaps_rollup -----'; \
cat /proc/$PID/smaps_rollup 2>/dev/null | grep -E 'Rss:|Pss:|Private_Clean:|Private_Dirty:|SwapPss:'; \
echo '----- status -----'; \
cat /proc/$PID/status 2>/dev/null | grep -E 'VmRSS|VmHWM|RssAnon|RssFile|RssShmem|VmSwap'; \
sleep 2; \
done"

分析顺序:

1. 先看同 PID;
2. 再看所有微信相关进程合计;
3. 优先看 Private Dirty、RssAnon;
4. 再看 PSS、Graphics、Private Other;
5. 结合 log marker 对齐操作阶段。

12.4 Perfetto / heapprofd

Perfetto 的 native heap profiling 可以按 malloc/free 调用栈聚合内存,但它只能看到 profiling 开始后的 native 分配,并且目标进程需要满足设备和权限条件。(Android Git Repositories)

在微信小游戏场景下限制较多:

1. 目标进程是微信,不是自己的 APK;
2. 普通 user 设备可能不能 profile 微信进程;
3. userdebug/root 设备更适合;
4. 仍需结合业务 marker 才能知道哪个阶段触发。

可用时,Perfetto 适合回答:

Native heap 增长到底来自哪个 native 调用栈?
是 malloc?
是 mmap?
是图形资源?
是线程栈?
是 JIT/code?

13. 常见高风险内存路径

13.1 截图 / readPixels

典型路径:

WebGL framebuffer
  -> readPixels
  -> JS Uint8Array
  -> Wasm encoder input
  -> JPEG/PNG output
  -> upload body

高风险点:

1. 每次 new Uint8Array(width × height × 4);
2. 多个 readback ring slot;
3. raw RGBA 同时存在 JS + Wasm + FS 三份;
4. 使用 canvas.toTempFilePath / readFile 产生宿主内部 copy;
5. base64 导出;
6. 上传回调闭包持有大 buffer;
7. GL texture/FBO/PBO 没有 delete。

优化:

1. readback buffer pool;
2. 限制 ring slot 数量;
3. 直接写入 Wasm HEAPU8.subarray;
4. 避免 raw RGBA 落 FS;
5. 上传完成后清理引用;
6. 显式 delete WebGL resources。

13.2 WebGL texture / framebuffer

危险模式:

const tex = gl.createTexture();
const fbo = gl.createFramebuffer();
// 使用后没有 gl.deleteTexture(tex)
// 使用后没有 gl.deleteFramebuffer(fbo)

专业建议:

所有 create* 都应有对应 delete*。
纹理缓存要有容量上限。
场景切换、功能关闭、Canvas 重建时要释放 GPU 资源。

13.3 FS 文件缓存

危险模式:

FS.writeFile("/tmp/large.raw", rawBytes);
// 忘记 FS.unlink

结果:

FS.nameTable
  -> FSNode.contents
    -> Uint8Array
      -> ArrayBuffer

优化:

1. 临时文件用完 unlink;
2. 不把 raw 大文件写入 FS;
3. 对 FS 文件做 size dump;
4. 对 FS.writeFile / FS.write 加 hook;
5. 避免在小游戏中滥用 Emscripten FS cache。

13.4 网络 response / upload body

危险模式:

response ArrayBuffer
  -> Uint8Array copy
  -> Wasm heap copy
  -> FS copy
  -> request body copy

优化:

1. 使用 view / subarray,避免 slice 复制;
2. 限制同时 inflight 请求;
3. 上传完成后释放 body 引用;
4. 不缓存完整历史包;
5. 大文件流式处理或分块处理。

13.5 Base64

危险模式:

binary image
  -> base64 string
  -> JSON
  -> upload

问题:

1. base64 体积膨胀;
2. 字符串在 JS heap 中明显变大;
3. JSON stringify 可能再产生副本;
4. 上传时可能再复制。

优化:

优先使用 ArrayBuffer / Uint8Array / Blob-like binary API。

13.6 事件监听和定时器

危险模式:

setInterval(...)
requestAnimationFrame(loop)
wx.onShow(callback)
wx.onHide(callback)
canvas.addEventListener(...)

重复注册但不注销,会导致:

closure retained
context retained
large state retained
module retained
buffer retained

优化:

1. start/stop 成对;
2. on/off 成对;
3. timer id 统一管理;
4. 多次 start 前先检查是否已经注册;
5. stop 后断开大对象引用。

14. 内存快照快速判断表

快照表现 专业含义 下一步
HEAPU8 -> ArrayBuffer 256MB Wasm linear memory / Unity Heap 看是否启动前已存在,是否 grow
FS -> nameTable -> contents -> Uint8Array Emscripten FS 文件内容 dump 路径、文件头、写入栈
system / JSArrayBufferData 很大 ArrayBuffer backing store 找 owner ArrayBuffer 和 retainer
Managed<wasm::NativeModule> 很大 Wasm 编译产物 减小 wasm、分包、懒加载
WasmDispatchTable / Table Wasm 函数表 通常不是泄露
string 源码、base64、JSON、日志 看内容和 retainer
system / Context 很多 闭包上下文 查事件、timer、Promise、模块缓存
Object retained 很大 容器支配子图 展开子节点,不要误判
WeakMap table 弱映射内部结构 注意 key/value 是否仍被强引用
JS heap 无增长但 Android 内存涨 native/GPU/mmap/宿主缓存 看 meminfo、smaps、Perfetto

15. 优化原则

15.1 延迟初始化

不要在启动阶段初始化所有重资源。

启动阶段只初始化核心逻辑。
截图、编码、上传、音频、复杂 WebGL FBO、离屏 Canvas 应按需初始化。

15.2 复用 buffer

适合复用:

readPixels buffer
YUV buffer
encode output buffer
network body buffer
decompression scratch
tile buffer
temporary typed arrays

注意:

复用不是无限缓存。
buffer pool 必须有最大容量和释放时机。

15.3 避免重复副本

优先:

view = HEAPU8.subarray(ptr, ptr + len);

谨慎:

array.slice()
buffer.slice()
Array.from()
JSON.stringify(largeObject)
base64 encode
FS.readFile()
FS.writeFile()

15.4 显式释放 WebGL 资源

gl.deleteTexture(tex);
gl.deleteBuffer(buf);
gl.deleteFramebuffer(fbo);
gl.deleteRenderbuffer(rbo);
gl.deleteProgram(program);
gl.deleteShader(shader);

同时清空 JS 引用:

tex = null;
fbo = null;

15.5 控制 Emscripten FS

临时文件:
  用完 unlink。

大 raw 数据:
  尽量不进 FS。

缓存文件:
  有容量限制和淘汰策略。

持久化:
  明确 sync 时机和内存副本成本。

15.6 控制 Wasm heap 峰值

1. 避免同一时间保留多个大 vector/string/buffer;
2. 分块处理大资源;
3. 降低启动阶段 malloc 峰值;
4. 调整 INITIAL_MEMORY / MAXIMUM_MEMORY;
5. 避免一次操作把 heap grow 到高水位;
6. 使用引擎 profiler 或 allocator 统计确认真实使用量。

15.7 用阶段差分而不是单点比较

专业测试报告应该写:

同一 PID 下,T0 到 T1:
Private Dirty 中位数 +X MB;
RssAnon 中位数 +Y MB;
JS heap retained +Z MB;
HEAPU8.buffer 未变化;
FS 大文件未新增;
Graphics +N MB;
持续 60 秒未回落。

不要写:

这次启动前 700MB,另一次 800MB,所以 SDK/模块涨了 100MB。

16. 推荐的分析流程模板

16.1 证明是否增长

1. 选定同一设备、同一微信版本、同一小游戏包;
2. 冷启动微信或清理 appbrand 进程;
3. 找到目标 PID;
4. 进入游戏并等待稳定;
5. 打 T0 marker;
6. 执行目标操作;
7. 打 T1/T2/T3 marker;
8. 每阶段等待 60 秒;
9. 采集 meminfo + smaps + heap snapshot;
10. 至少重复 5 次,取中位数。

16.2 证明是否泄露

for i in 1..10:
  start feature
  wait stable
  stop feature
  wait stable
  collect memory

判断:

每轮结束后的稳定基线持续上升:
  泄露或未释放缓存。

第一轮上升后稳定:
  驻留成本。

操作期间上升,结束后回落:
  峰值成本。

16.3 归因路径

JS heap 增长:
  heap snapshot / retainer / allocation stack。

Wasm heap capacity 增长:
  HEAPU8.buffer.byteLength / sbrk / malloc stats。

FS 增长:
  FS dump / file path / file head / FS.write hook。

GPU/Graphics 增长:
  WebGL resource counter / delete audit / dumpsys Graphics / GL mtrack。

Native heap 增长:
  dumpsys Native Heap / smaps RssAnon / Perfetto heapprofd。

Private Other 增长:
  XWeb/宿主/图形/匿名 mmap,需要 marker + Perfetto + 功能开关实验。

17. 标准术语表

术语 专业解释
JS Heap JavaScript 引擎管理的对象堆
GC Root 垃圾回收根对象,从它可达的对象不会被回收
Shallow Size / Self Size 对象自身大小
Retained Size 对象释放后可能连带释放的大小
Retainer Path 当前是谁持有该对象
Allocation Stack 对象创建时的调用栈
ArrayBuffer JS 二进制缓冲区对象
JSArrayBufferData ArrayBuffer 的 native backing store
TypedArray Uint8Array、Float32Array 等 ArrayBuffer 视图
WebAssembly.Memory Wasm 线性内存对象
HEAPU8 Emscripten 对 Wasm memory 的 Uint8Array 视图
Module Emscripten runtime 全局对象
MEMFS Emscripten 内存文件系统
IDBFS IndexedDB-backed Emscripten 文件系统
FSNode.contents Emscripten FS 文件内容
Wasm NativeModule JS 引擎中的 Wasm 编译产物
WebGLTexture WebGL 纹理 wrapper
Framebuffer 离屏或目标渲染缓冲组合
Renderbuffer 深度/模板/多采样渲染缓冲
PBO Pixel Buffer Object,用于像素传输
PSS 共享页按比例分摊后的进程内存
RSS 进程驻留物理内存
Private Dirty 进程私有且不可直接丢弃的脏页
RssAnon 匿名驻留内存,常对应 malloc/Wasm/native
RssFile 文件映射驻留内存
RssShmem 共享内存驻留
Graphics / GL mtrack 图形/GPU/EGL/OpenGL 相关内存
Private Other Android meminfo 中不易归类的私有内存

18. 最重要的心智模型

分析 WebGL / 微信小游戏内存时,应该始终把内存分成四个层面:

1. JS 可见对象
   Object / Array / Closure / String / ArrayBuffer wrapper。

2. JS 外部 backing store
   system / JSArrayBufferData,大型二进制数据。

3. Wasm / Emscripten / Unity runtime
   HEAPU8、Unity Heap、FS、Wasm compiled module、Table。

4. 宿主和系统层
   WebGL/GPU、Canvas、XWeb、微信小游戏 runtime、Android native heap、Graphics、Private Other。

一个 solid 的内存结论必须回答:

1. 增长发生在哪个阶段?
2. 增长属于 JS heap、Wasm heap、FS、GPU、native,还是宿主缓存?
3. 是峰值、驻留,还是泄露?
4. 是同一进程内稳定复现,还是跨运行波动?
5. 快照中的 retainer path 证明了什么?
6. allocation stack 或打点是否证明了分配来源?
7. 这块内存对功能是否必要?
8. 是否过早分配?
9. 是否可以延迟、复用、缩容或释放?

最终,专业表述应从:

“启动后内存涨了 50MB”

升级为:

“在同一 appbrand 进程内,目标功能启动后 60 秒稳定区间,Private Dirty 中位数增加 28MB,RssAnon 增加 31MB;JS heap snapshot 未新增大型 retained object,HEAPU8.buffer 未增长,FS 大文件未新增;因此该增长更可能位于 native / graphics / 宿主 runtime 层。下一步应按 WebGL resource、Canvas readback、网络 body、native heap 四类路径继续分解。”

这就是 WebGL / 微信小游戏内存分析中最重要的专业化思考方式。