piotr-oles/as-proto

Use of `unmanaged` on generated classes causing memory leaks

Closed this issue · 3 comments

Hi,
We ran into an issue where the @unmanaged decorator that is added to certain generated classes causes a memory leak when decoding objects.

Specifically, we have a .proto file like

message NullableDouble {
  double value = 1;
  bool isNull = 2;
  string dummy = 3;
}

which results in Assemblyscript code like

// Code generated by protoc-gen-as. DO NOT EDIT.
// Versions:
//   protoc-gen-as v1.2.0
//   protoc        v3.21.12

import { Writer, Reader, Protobuf } from "as-proto/assembly";

@unmanaged
export class NullableDouble {
  static encode(message: NullableDouble, writer: Writer): void {
    writer.uint32(9);
    writer.double(message.value);

    writer.uint32(16);
    writer.bool(message.isNull);
  }

  static decode(reader: Reader, length: i32): NullableDouble {
    const end: usize = length < 0 ? reader.end : reader.ptr + length;
    const message = new NullableDouble();

    while (reader.ptr < end) {
      const tag = reader.uint32();
      switch (tag >>> 3) {
        case 1:
          message.value = reader.double();
          break;

        case 2:
          message.isNull = reader.bool();
          break;

        default:
          reader.skipType(tag & 7);
          break;
      }
    }

    return message;
  }

  value: f64;
  isNull: bool;

  constructor(value: f64 = 0.0, isNull: bool = false, dummy: string = "") {
    this.value = value;
    this.isNull = isNull;
  }
}

export function encodeNullableDouble(message: NullableDouble): Uint8Array {
  return Protobuf.encode(message, NullableDouble.encode);
}

export function decodeNullableDouble(buffer: Uint8Array): NullableDouble {
  return Protobuf.decode<NullableDouble>(buffer, NullableDouble.decode);
}

Since the decode function creates a new instance of NullableDouble each time (which is an unmanaged class) it appears to lead to constant growth of memory where these instances are not garbage collected. In ours tests, Assemblyscript would keep expanding the memory it had reserved, even while the actual "known" size of the managed memory heap was not increasing.

Did you report that to the AssemblyScript team? My understanding of unmanaged classes is that they are equivalent to structs and that they are stored on a stack instead of the heap, so they don’t have to be garbage collected. I might miss something for sure :)

@piotr-oles I asked on the discord here: https://discord.com/channels/721472913886281818/1086289901315698718

it sounds like it does need to be manually freed to prevent leaks

Ok, thanks for checking that! Then it's a bug on the library side indeed. Feel free to create a PR that removes the generation of unmanaged classes - it should be a pretty simple change.