Skip to content

Nigh unstoppable memory leaks from repeated application of immutable pipelined textures (Texture.delete() does not seem to work) #815

@Aloeminium108

Description

@Aloeminium108

What is wrong?

While other textures seem to be deleting properly, if I apply the same immutable pipelined kernel over and over again, it causes a memory leak.

Where does it happen?

I've been using GPU.js version 2.16.0 (though reverting to other versions has not helped) in an app that I've been testing in Chromium and Firefox with Browserify on Arch Linux. The app is written in TypeScript and uses Express/React.

How do we replicate the issue?

WARNING: this code causes a memory leak, execute at your own risk; reducing the size or numIterations variables will change the size of the leak.
Also, in my use case, this code is being executed once a frame. In order to diagnose the issue without causing Chromium to crash, I've capped the framerate in my app to one frame a second. So, the loop that causes the issue is being executed once every second. (Creating the GPU and the kernels happens outside of the animation frames, so they are not the source of the leak)

const size = 256
const numIterations = 30

const gpu = new GPU()

const generateInput = gpu.createKernel(function() {
   return [0, 0]
})
   .setOutput([size, size])
   .setPipeline(true)

const kernel = gpu.createKernel(function(input: number[][][]) {
   const result = input[this.thread.y][this.thread.x]
   return [result[0] + 1, result[1] + 1]
})
   .setOutput([size, size])
   .setPipeline(true)
   .setImmutable(true)
   .setArgumentTypes({ input: 'Array2D(2)' })

const input = generateInput()
kernel(input)

let frameCount = 0
let prevTimeStamp = Date.now()

const animate = () => {

    frameCount++

    const timeStamp = Date.now()

    if (timeStamp - prevTimeStamp >= 1000) {
        prevTimeStamp = timeStamp
        frameCount = 0
    }

    if (frameCount === 1) {
        
        // Loop where memory leak happens
        for (let x = 0; x < numIterations; x++) {
            let prevPass = kernel.texture
            kernel(kernel.texture)
            prevPass.delete()
        }

    }

    requestAnimationFrame(animate)

}

I will note that changing the line from

const prevPass = kernel.texture

to

const prevPass = kernel.texture.clone()

makes no difference.

I have gone in circles attempting everything under the sun to fix this. I can believe that there's something I haven't tried yet, but I would be genuinely surprised. There are only two things that I've found that have seemed to help, but both of those solutions lead me to believe there maybe an issue with GPU.js itself.

How important is this (1-5)?

To me this is a 5. The algorithm that I'm trying to use in the actual app just can't work without repeated passes in some way. I haven't been able to find any workaround that will produce the same result while avoiding the memory leak. Also, since the loop needs to be called every frame, I need the kernel to be pipelined.

Expected behavior (i.e. solution)

The expected behavior is for there to be no memory leak from a loop like this, so long as the textures are deleted appropriately.

As mentioned before, there are two "solutions" I've found, but neither are very satisfactory.

The first "solution" is to go into into the module itself and modify it.
In gpu.js/src/backend/gl/texture/index.js there is a block of code:

  delete() {
    if (this._deleted) return;
    this._deleted = true;
    if (this.texture._refs) {
      this.texture._refs--;
      if (this.texture._refs) return;
    }
    this.context.deleteTexture(this.texture);
    // TODO: Remove me
    // if (this.texture._refs === 0 && this._framebuffer) {
    //   this.context.deleteFramebuffer(this._framebuffer);
    //   this._framebuffer = null;
    // }
  }

After deleting (in practice I've only ever commented it out) the if (this.texture._refs)... block so it looks like this:

  delete() {
    if (this._deleted) return;
    this._deleted = true;
    this.context.deleteTexture(this.texture);
    // TODO: Remove me
    // if (this.texture._refs === 0 && this._framebuffer) {
    //   this.context.deleteFramebuffer(this._framebuffer);
    //   this._framebuffer = null;
    // }
  }

Suddenly the memory leak is gone.

The second "solution" I've found is to attempt to delete the textures from previous passes more forcefully by changing the loop to

for (let x = 0; x < numIterations; x++) {
   const prevPass = kernel.texture
   kernel(kernel.texture)
   kernel.context.deleteTexture(prevPass.texture)
}

Of course, that internal texture field is not public in Texture (At least in the version of GPU.js I'm using), so the TypeScript compiler doesn't like this. But it does seem to work. I saw that doing Texture.clear() also decrements _refs, so I've tried doing prevPass.clear() before prevPass.delete() but it doesn't seem to work.

Other Comments

If the real issue is that I'm using something wrong, then I would greatly appreciate any advice on how to get this to work without a memory leak.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions