最近我一直在进行一个项目,旨在创建一个实时高性能的JavaScript图表库。该项目使用了一个雄心勃勃且新颖的技术堆栈,其中包括一个大型的C/C++遗留代码库,使用Emscripten编译为WebAssembly,针对WebGL进行渲染,并提供TypeScript API包装器,可在JavaScript中加载图表而无需担心底层的Wasm。
为什么要使用WebAssembly?
WebAssembly是一项令人兴奋的技术,在许多情况下比JavaScript具有更好的性能优势。而且,在这种情况下,由于遗留的C++代码库已经处理了OpenGL中的大部分图表渲染工作,只需要做一些工作即可将其定位到WebGL。
使用Emscripten将现有的C++代码编译成WebAssembly非常简单,只需编写绑定生成类型并在Wasm库周围编写JavaScript API即可使用它。
在开发库的过程中,我们学到了一些关于WebAssembly内存模型的有趣事实,以及如何避免和调试内存泄漏,下面我将分享给大家。
JavaScript与WebAssembly内存模型的区别
WebAssembly与JavaScript具有完全不同的内存模型。虽然JavaScript拥有自动垃圾回收器,可以自动清理不再需要的变量的内存,但WebAssembly则没有。在Wasm内存中声明的对象或缓冲区必须由调用者删除,否则将导致内存泄漏。
JavaScript中的内存泄漏是如何引起的
无论是JavaScript还是WebAssembly,都可能发生内存泄漏,开发人员必须小心确保在使用WebAssembly时正确清理内存。
尽管JavaScript是一种带有垃圾回收机制的托管式编程语言,但使用纯粹的JavaScript仍然很容易创建内存泄漏。以下是在JavaScript应用程序中无意中导致内存泄漏的几种方式:
箭头函数和闭包可以捕获变量并使其保持活动状态,因此无法被JavaScript垃圾回收器删除。
回调函数或事件监听器可以捕获变量并保持其活动状态。
全局变量或静态变量在应用程序的生命周期中一直存在。如果忘记使用let或const关键字,变量将转换为全局变量。
即使从DOM中分离的节点也可以在JavaScript中保持对象的活动状态。仅仅移除一个节点但保留对它的引用变量,将阻止该节点及其子节点被回收。
WebAssembly中的内存泄漏是如何引起的
Wasm拥有与JavaScript虚拟机不同的堆内存。该内存在浏览器中分配,并从主机操作系统中保留。当您在Wasm中分配内存时,Wasm堆会增长,并且保留了一定范围的地址。当您删除Wasm中的内存时,堆不会缩小,并且内存也不会返回给主机操作系统。相反,内存只是被标记为已删除或可用。这意味着它可以被未来的分配重新使用。
要在WebAssembly中引起内存泄漏,只需分配内存并忘记删除它即可。由于没有自动垃圾回收、终结或将内存标记为不再需要的机制,必须由用户来处理。由编译器Emscripten导出的所有WebAssembly类型都具有一种在使用Wasm内存的对象上调用的`.delete()`函数。当不再需要该对象时,需要调用此函数进行删除。以下是一个快速示例:
示例:Wasm中的内存泄漏
假设您在C++中声明了如下类型:
// person.cpp #include <string> class Person { public: // C++ 构造函数 Person(std::string name, int age) : name(name), age(age) {} // C++ 析构函数 ~Person() {} std::string getName() { return name; } int getAge() { return age; } private: std::string name; int age; };
`然后使用Emscripten编译和导出该类型,如下所示:
emcc person.cpp -o person.js -s EXPORTED_FUNCTIONS="['_createPerson', '_deletePerson', '_getName', '_getAge']" -s MODULARIZE=1
现在,您可以在JavaScript中实例化、使用和删除该类型,如下所示:
const Module = require('./person.js'); // 包含生成的JavaScript接口 Module.onRuntimeInitialized = () => { // 实例化一个Person对象 const person = new Module.Person('John Doe', 30); console.log('创建Person对象:', person); // 访问并打印属性 console.log('姓名:', person.getName()); console.log('年龄:', person.getAge()); // 删除Person对象(调用C++析构函数) person.delete(); };
然而,如果忘记调用`.delete()`函数,就会导致Wasm内存泄漏。浏览器的内存将增长而不会缩小。
检测WebAssembly应用程序中的内存泄漏
由于内存泄漏对应用程序来说是灾难性的,我们不仅要确保我们的代码不会泄漏内存,还要确保用户代码(即使用我们的JavaScript图表库的应用程序)不会泄漏内存。
为此,我们开发了内部内存调试工具。它实现为一个对象注册表,其中包含所有未删除和未收集的对象的Map<string, TObjectEntryInfo>,其中TObjectEntryInfo是一个存储对象的WeakRef的类型。
通过使用JavaScript代理技术,我们能够拦截对所有WebAssembly类型的new/delete的调用。每次实例化一个对象时,我们将其添加到objectRegistry中;每次删除一个对象时,我们将其从objectRegistry中移除。
现在,您可以运行应用程序,启用内存调试工具,并输出应用程序状态的特定快照。以下是该工具输出的示例。
首先,启用MemoryUsageHelper(内存调试工具):
import { MemoryUsageHelper } from "scichart"; MemoryUsageHelper.isMemoryUsageDebugEnabled = true;
这将自动跟踪我们库中的所有类型,但您也可以通过调用register和unregister来跟踪应用程序中的任意对象:
// 注册一个任意对象 MemoryUsageHelper.register(yourObject, "identifier"); // 注销一个任意对象 MemoryUsageHelper.unregister("identifier");
稍后,在特定的点上通过调用此函数输出一个快照:文章来源:https://www.toymoban.com/diary/apps/652.html
MemoryUsageHelper.objectRegistry.log();
这将在控制台输出所有未被删除或未收集的对象和它们的标识符。通过检查这个快照,您可以确定是否有任何内存泄漏。文章来源地址https://www.toymoban.com/diary/apps/652.html
到此这篇关于如何管理用于高规模服务的单写数据库管理系统?的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!