From e8fddba4945c0d6f44cd532bedd30c8be1b93f72 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 25 Oct 2018 16:48:17 -0700 Subject: [PATCH] New JS test for memory leaks. This test recreates a renderable every few frames and dumps the heap pointer to the console so we can check if it increases over time. Also simplified the personal web server script so that it uses twisted and avoid doing a chdir, seems to be more reliable this way. --- libs/filamentjs/CMakeLists.txt | 1 + libs/filamentjs/tests/test.py | 48 ++++----- libs/filamentjs/tests/test_leaks.html | 18 ++++ libs/filamentjs/tests/test_leaks.js | 135 ++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 31 deletions(-) create mode 100644 libs/filamentjs/tests/test_leaks.html create mode 100644 libs/filamentjs/tests/test_leaks.js diff --git a/libs/filamentjs/CMakeLists.txt b/libs/filamentjs/CMakeLists.txt index 58d5055355..24d1cbfe90 100644 --- a/libs/filamentjs/CMakeLists.txt +++ b/libs/filamentjs/CMakeLists.txt @@ -16,6 +16,7 @@ set(CPP_SRC set(COPTS "${COPTS} -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0") set(LOPTS "${LOPTS} --bind") +set(LOPTS "${LOPTS} -s FILESYSTEM=0") set(LOPTS "${LOPTS} -s ALLOW_MEMORY_GROWTH=1") set(LOPTS "${LOPTS} -s MODULARIZE_INSTANCE=1") set(LOPTS "${LOPTS} -s EXPORT_NAME=Filament") diff --git a/libs/filamentjs/tests/test.py b/libs/filamentjs/tests/test.py index f21e0c0051..58dfaf254e 100755 --- a/libs/filamentjs/tests/test.py +++ b/libs/filamentjs/tests/test.py @@ -1,20 +1,23 @@ #!/usr/bin/env python3 import glob -import http.server import os import shutil -import socketserver import sys from pathlib import Path +from twisted.web.server import Site, GzipEncoderFactory +from twisted.web.static import File +from twisted.web.resource import Resource, EncodingResourceWrapper +from twisted.internet import reactor + from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from watchdog.events import LoggingEventHandler SCRIPT_DIR = Path(os.path.dirname(os.path.abspath(__file__))) -ROOT_DIR = Path(os.path.realpath(SCRIPT_DIR / "../../../")) +ROOT_DIR = Path(os.path.realpath(SCRIPT_DIR / "../../..")) BUILD_DIR = ROOT_DIR / "out/cmake-webgl-release" TOOLS_DIR = ROOT_DIR / "out/cmake-release/tools" SERVE_DIR = BUILD_DIR / "libs/filamentjs" @@ -22,26 +25,15 @@ GLMATRIX_DIR = ROOT_DIR / "third_party/gl-matrix" CURR_DIR = Path(os.path.realpath('.')) PORT = 8000 -global server - # Restart the script if the JavaScript or HTML change. class OnDirectoryChanged(FileSystemEventHandler): def on_modified(self, event): - global server if event.event_type != 'modified': return - server.shutdown() print(f'{event.src_path} was modified\nRestarting...') - os.chdir(CURR_DIR) os.execl(sys.executable, *([sys.executable]+sys.argv)) -print("Starting observer...") -handler = OnDirectoryChanged() -observer = Observer() -observer.schedule(handler, path=str(CURR_DIR), recursive=True) -observer.start() - # Copy test assets into the server folder. print("Copying assets...") @@ -99,26 +91,20 @@ shutil.copy(parquet_filamesh, SERVE_DIR) for tex in parquet_textures: shutil.copy(tex, SERVE_DIR) -# Associate wasm files with the correct MIME type. - -Handler = http.server.SimpleHTTPRequestHandler -Handler.extensions_map.update({ - '.wasm': 'application/wasm', -}) - # Serve all files in the server folder. print(f"Serving {SERVE_DIR}...") print(f" http://localhost:{PORT}/test_redball.html") print(f" http://localhost:{PORT}/test_parquet.html") -Handler.directory = SERVE_DIR -os.chdir(SERVE_DIR) -socketserver.TCPServer.allow_reuse_address = True -with socketserver.TCPServer(("", PORT), Handler) as httpd: - global server - server = httpd - httpd.allow_reuse_address = True - httpd.serve_forever() +print(f" http://localhost:{PORT}/test_leaks.html") -observer.stop() -observer.join() +port = 8000 +webdir = File(SERVE_DIR) +webdir.contentTypes['.wasm'] = 'application/wasm' +wrapped = EncodingResourceWrapper(webdir, [GzipEncoderFactory()]) +reactor.listenTCP(port, Site(wrapped)) +handler = OnDirectoryChanged() +observer = Observer() +observer.schedule(handler, path=str(CURR_DIR), recursive=True) +observer.start() +reactor.run() diff --git a/libs/filamentjs/tests/test_leaks.html b/libs/filamentjs/tests/test_leaks.html new file mode 100644 index 0000000000..95356d48b1 --- /dev/null +++ b/libs/filamentjs/tests/test_leaks.html @@ -0,0 +1,18 @@ + + + + FilamentJS Test + + + + + + + + + + + \ No newline at end of file diff --git a/libs/filamentjs/tests/test_leaks.js b/libs/filamentjs/tests/test_leaks.js new file mode 100644 index 0000000000..da8db4ac5b --- /dev/null +++ b/libs/filamentjs/tests/test_leaks.js @@ -0,0 +1,135 @@ +Filament.init([ 'bakedColor.filamat' ], () => { + window.VertexAttribute = Filament.VertexAttribute; + window.AttributeType = Filament.VertexBuffer$AttributeType; + window.PrimitiveType = Filament.RenderableManager$PrimitiveType; + window.IndexType = Filament.IndexBuffer$IndexType; + window.Fov = Filament.Camera$Fov; + window.LightType = Filament.LightManager$Type; + const canvas = document.getElementsByTagName('canvas')[0]; + window.app = new App(canvas); +}); + +class App { + constructor(canvas) { + const BAKED_COLOR_PACKAGE = Filament.Buffer(Filament.assets['bakedColor.filamat']); + this.canvas = canvas; + this.engine = Filament.Engine.create(this.canvas); + this.mi = this.engine.createMaterial(BAKED_COLOR_PACKAGE).getDefaultInstance(); + const engine = this.engine; + this.polygon = Filament.EntityManager.get().create(); + this.createPolygon(this.polygon, 3, this.mi); + this.scene = engine.createScene(); + this.scene.addEntity(this.polygon); + this.swapChain = engine.createSwapChain(); + this.renderer = engine.createRenderer(); + this.camera = engine.createCamera(); + this.view = engine.createView(); + this.view.setCamera(this.camera); + this.view.setScene(this.scene); + this.resize(); + this.render = this.render.bind(this); + this.resize = this.resize.bind(this); + window.addEventListener("resize", this.resize); + window.requestAnimationFrame(this.render); + } + + render() { + this.animate(); + this.renderer.render(this.swapChain, this.view); + window.requestAnimationFrame(this.render); + } + + resize() { + const dpr = window.devicePixelRatio; + const width = this.canvas.width = window.innerWidth * dpr; + const height = this.canvas.height = window.innerHeight * dpr; + this.view.setViewport([0, 0, width, height]); + const eye = [0, 0, 3], center = [0, 0, -1], up = [0, 1, 0]; + this.camera.lookAt(eye, center, up); + const aspect = width / height; + this.camera.setProjectionFov(45, aspect, 1.0, 10.0, aspect < 1 ? Fov.HORIZONTAL : Fov.VERTICAL); + } + + createPolygon(polygon, nsides, minstance) { + const engine = this.engine; + const nverts = nsides + 1; + const ntris = nsides; + const dtheta = Math.PI * 2 / nsides; + // Create typed arrays. + const indices = new Uint16Array(3 * ntris); + const positions = new Float32Array(2 * nverts); + const colors = new Uint32Array(nverts); + const palette = new Uint32Array([0xffff0000, 0xff00ff00, 0xff0000ff]); + for (let i = 1, j = 0, theta = 0; i < nverts; i++, theta += dtheta) { + positions[i * 2] = Math.cos(theta); + positions[i * 2 + 1] = Math.sin(theta); + colors[i] = palette[i % 3]; + indices[j++] = 0; + indices[j++] = i; + indices[j++] = 1 + (i % (nverts - 1)); + } + // Create vertex buffer. + const vbuilder = Filament.VertexBuffer.Builder() + .vertexCount(nverts) + .bufferCount(2) + .attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT2, 0, 8) + .attribute(VertexAttribute.COLOR, 1, AttributeType.UBYTE4, 0, 4) + .normalized(VertexAttribute.COLOR); + const vb = vbuilder.build(engine); + vbuilder.delete(); + const pbuf = Filament.Buffer(positions); + const cbuf = Filament.Buffer(colors); + vb.setBufferAt(engine, 0, pbuf); + vb.setBufferAt(engine, 1, cbuf); + pbuf.delete(); + cbuf.delete(); + // Create index buffer. + const ibuilder = Filament.IndexBuffer.Builder() + .indexCount(indices.length) + .bufferType(IndexType.USHORT); + const ib = ibuilder.build(engine); + ibuilder.delete(); + const ibuf = Filament.Buffer(indices); + ib.setBuffer(engine, ibuf); + ibuf.delete(); + // Create renderable component. + const rbuilder = Filament.RenderableManager.Builder(1) + .boundingBox([ [-1, -1, -1], [1, 1, 1] ]) + .material(0, minstance) + .geometry(0, PrimitiveType.TRIANGLES, vb, ib); + rbuilder.build(engine, polygon); + rbuilder.delete(); + vb.delete(); + ib.delete(); + } + + // For now, simply print out malloc'd pointer value and check + // if it increases over time. Is there a better way of doing this? + printMemoryUsage() { + const ptr = Filament._malloc(1); + Filament._free(ptr); + this.maxptr = Math.max(this.maxptr | 0, ptr); + console.log(this.maxptr); + } + + animate() { + // Recreate the polygon every couple frames, changing the number of sides. + this.frame = (this.frame | 0) + 1; + if (this.frame > 2) { + this.frame = 0; + this.nsides = (this.nsides | 0) + 1; + this.nsides = this.nsides % 10; + // Destroy and recreate the renderable component. + this.engine.destroyEntity(this.polygon); + this.createPolygon(this.polygon, 3 + this.nsides, this.mi); + this.printMemoryUsage(); + } + // Rotate the polygon around the Z axis. + const radians = Date.now() / 1000; + const transform = mat4.fromRotation(mat4.create(), radians, [0, 0, 1]); + const tcm = this.engine.getTransformManager(); + const inst = tcm.getInstance(this.polygon); + tcm.setTransform(inst, transform); + inst.delete(); + } +}