This is an automated email from the ASF dual-hosted git repository.
ruihangl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git
The following commit(s) were added to refs/heads/main by this push:
new 1e988a4a6f [WEB] Reduce memleak in web runtime (#14086)
1e988a4a6f is described below
commit 1e988a4a6f2e08518343cfffdeab48e01bae15d1
Author: Tianqi Chen <[email protected]>
AuthorDate: Wed Feb 22 15:20:21 2023 -0500
[WEB] Reduce memleak in web runtime (#14086)
This PR robustifies the web runtime to reduce memory leak
and enhances the runtime with object support.
Specifically we introduce scoping and auto-release
mechanism when we exit the scope. The improvements
are helpful to deal with memory leak in wasm and webgpu settings
---
web/.eslintignore | 1 +
web/README.md | 2 +-
web/apps/node/example.js | 2 +
web/emcc/wasm_runtime.cc | 2 +-
web/src/ctypes.ts | 22 +
web/src/index.ts | 5 +-
web/src/rpc_server.ts | 19 +-
web/src/runtime.ts | 642 ++++++++++++++++-----
web/tests/node/test_module_load.js | 15 +-
web/tests/node/test_ndarray.js | 16 +-
web/tests/node/{test_ndarray.js => test_object.js} | 35 +-
web/tests/node/test_packed_func.js | 59 +-
web/tests/python/websock_rpc_test.py | 1 -
13 files changed, 637 insertions(+), 184 deletions(-)
diff --git a/web/.eslintignore b/web/.eslintignore
index 1521c8b765..f71ee79871 100644
--- a/web/.eslintignore
+++ b/web/.eslintignore
@@ -1 +1,2 @@
dist
+debug
diff --git a/web/README.md b/web/README.md
index 4154300e62..64f507579e 100644
--- a/web/README.md
+++ b/web/README.md
@@ -81,7 +81,7 @@ The following is an example to reproduce this.
- Start the WebSocket RPC
- Browswer version: open https://localhost:8888, click connect to proxy
- NodeJS version: `npm run rpc`
-- run `python tests/node/websock_rpc_test.py` to run the rpc client.
+- run `python tests/python/websock_rpc_test.py` to run the rpc test.
## WebGPU Experiments
diff --git a/web/apps/node/example.js b/web/apps/node/example.js
index cff76d8a06..0cd6b53201 100644
--- a/web/apps/node/example.js
+++ b/web/apps/node/example.js
@@ -31,8 +31,10 @@ const wasmSource = fs.readFileSync(path.join(wasmPath,
"tvmjs_runtime.wasm"));
// the async version of the API.
tvmjs.instantiate(wasmSource, new EmccWASI())
.then((tvm) => {
+ tvm.beginScope();
const log_info = tvm.getGlobalFunc("testing.log_info_str");
log_info("hello world");
// List all the global functions from the runtime.
console.log("Runtime functions using EmccWASI\n",
tvm.listGlobalFuncNames());
+ tvm.endScope();
});
diff --git a/web/emcc/wasm_runtime.cc b/web/emcc/wasm_runtime.cc
index 2b0ee49d7e..00d2a8c579 100644
--- a/web/emcc/wasm_runtime.cc
+++ b/web/emcc/wasm_runtime.cc
@@ -32,10 +32,10 @@
#include <tvm/runtime/logging.h>
#include "src/runtime/c_runtime_api.cc"
+#include "src/runtime/container.cc"
#include "src/runtime/contrib/sort/sort.cc"
#include "src/runtime/cpu_device_api.cc"
#include "src/runtime/file_utils.cc"
-#include "src/runtime/graph_executor/graph_executor.cc"
#include "src/runtime/library_module.cc"
#include "src/runtime/logging.cc"
#include "src/runtime/module.cc"
diff --git a/web/src/ctypes.ts b/web/src/ctypes.ts
index 4a6d25ae62..282679fc02 100644
--- a/web/src/ctypes.ts
+++ b/web/src/ctypes.ts
@@ -46,6 +46,7 @@ export type FTVMModGetFunction = (
* TVMModuleHandle dep);
*/
export type FTVMModImport = (mod: Pointer, dep: Pointer) => number;
+
/**
* int TVMModFree(TVMModuleHandle mod);
*/
@@ -161,6 +162,27 @@ export type FTVMBackendPackedCFunc = (
argValues: Pointer, argCodes: Pointer, nargs: number,
outValue: Pointer, outCode: Pointer) => number;
+
+/**
+ * int TVMObjectFree(TVMObjectHandle obj);
+ */
+ export type FTVMObjectFree = (obj: Pointer) => number;
+
+/**
+ * int TVMObjectGetTypeIndex(TVMObjectHandle obj, unsigned* out_tindex);
+ */
+export type FTVMObjectGetTypeIndex = (obj: Pointer, out_tindex: Pointer) =>
number;
+
+/**
+ * int TVMObjectTypeIndex2Key(unsigned tindex, char** out_type_key);
+ */
+export type FTVMObjectTypeIndex2Key = (type_index: number, out_type_key:
Pointer) => number;
+
+/**
+ * int TVMObjectTypeKey2Index(const char* type_key, unsigned* out_tindex);
+ */
+export type FTVMObjectTypeKey2Index = (type_key: Pointer, out_tindex: Pointer)
=> number;
+
// -- TVM Wasm Auxiliary C API --
/** void* TVMWasmAllocSpace(int size); */
diff --git a/web/src/index.ts b/web/src/index.ts
index ac82e5967f..bf2d982e21 100644
--- a/web/src/index.ts
+++ b/web/src/index.ts
@@ -19,8 +19,9 @@
export {
Scalar, DLDevice, DLDataType,
- PackedFunc, Module, NDArray, Instance,
- instantiate
+ PackedFunc, Module, NDArray,
+ TVMArray,
+ Instance, instantiate
} from "./runtime";
export { Disposable, LibraryProvider } from "./types";
export { RPCServer } from "./rpc_server";
diff --git a/web/src/rpc_server.ts b/web/src/rpc_server.ts
index c63dcf3a9a..e37d1838d6 100644
--- a/web/src/rpc_server.ts
+++ b/web/src/rpc_server.ts
@@ -22,6 +22,8 @@ import { assert, StringToUint8Array, Uint8ArrayToString }
from "./support";
import { detectGPUDevice } from "./webgpu";
import * as compact from "./compact";
import * as runtime from "./runtime";
+import { timeStamp } from "console";
+import { Disposable } from "./types";
enum RPCServerState {
InitHeader,
@@ -83,6 +85,7 @@ export class RPCServer {
private pendingSend: Promise<void> = Promise.resolve();
private name: string;
private inst?: runtime.Instance = undefined;
+ private globalObjects: Array<Disposable> = [];
private serverRecvData?: (header: Uint8Array, body: Uint8Array) => void;
private currPacketHeader?: Uint8Array;
private currPacketLength = 0;
@@ -121,6 +124,9 @@ export class RPCServer {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private onClose(_event: CloseEvent): void {
if (this.inst !== undefined) {
+ this.globalObjects.forEach(obj => {
+ obj.dispose();
+ });
this.inst.dispose();
}
if (this.state == RPCServerState.ReceivePacketHeader) {
@@ -263,6 +269,9 @@ export class RPCServer {
}
this.inst = inst;
+ // begin scope to allow handling of objects
+ // the object should stay alive during all sessions.
+ this.inst.beginScope();
const fcreate = this.inst.getGlobalFunc("rpc.CreateEventDrivenServer");
const messageHandler = fcreate(
@@ -301,8 +310,10 @@ export class RPCServer {
this.name,
this.key
);
-
- fcreate.dispose();
+ // message handler should persist across RPC runs
+ this.globalObjects.push(
+ this.inst.detachFromCurrentScope(messageHandler)
+ );
const writeFlag = this.inst.scalar(3, "int32");
this.serverRecvData = (header: Uint8Array, body: Uint8Array): void => {
@@ -320,7 +331,6 @@ export class RPCServer {
// register the callback to redirect the session to local.
const flocal = this.inst.getGlobalFunc("wasm.LocalSession");
const localSession = flocal();
- flocal.dispose();
assert(localSession instanceof runtime.Module);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -333,13 +343,14 @@ export class RPCServer {
);
messageHandler(header, writeFlag);
messageHandler(body, writeFlag);
- localSession.dispose();
this.log("Finish initializing the Wasm Server..");
this.requestBytes(SizeOf.I64);
this.state = RPCServerState.ReceivePacketHeader;
// call process events in case there are bufferred data.
this.processEvents();
+ // recycle all values.
+ this.inst.endScope();
};
this.state = RPCServerState.WaitForCallback;
diff --git a/web/src/runtime.ts b/web/src/runtime.ts
index b341a7d4b1..a24459ca29 100644
--- a/web/src/runtime.ts
+++ b/web/src/runtime.ts
@@ -29,6 +29,7 @@ import { WebGPUContext } from "./webgpu";
import * as compact from "./compact";
import * as ctypes from "./ctypes";
+import { tsImportEqualsDeclaration } from "@babel/types";
/**
* Type for PackedFunc inthe TVMRuntime.
@@ -134,6 +135,95 @@ class FFILibrary implements Disposable {
}
}
+/**
+ * @internal
+ * Manages extra runtime context for the runtime.
+ */
+class RuntimeContext implements Disposable {
+ arrayGetItem : PackedFunc;
+ arrayGetSize : PackedFunc;
+ arrayMake : PackedFunc;
+ getSysLib: PackedFunc;
+
+ private autoDisposeScope: Array<Array<Disposable | undefined>> = [];
+
+ constructor(getGlobalFunc: (name: string) => PackedFunc) {
+ this.arrayGetItem = getGlobalFunc("runtime.ArrayGetItem");
+ this.arrayGetSize = getGlobalFunc("runtime.ArraySize");
+ this.arrayMake = getGlobalFunc("runtime.Array");
+ this.getSysLib = getGlobalFunc("runtime.SystemLib");
+ }
+
+ dispose(): void {
+ this.arrayGetItem.dispose();
+ this.arrayGetSize.dispose();
+ this.arrayMake.dispose();
+ }
+
+ beginScope() : void {
+ this.autoDisposeScope.push([]);
+ }
+
+ endScope() : void {
+ if (this.autoDisposeScope.length == 0) {
+ throw Error("tvm.endScope called when the stack is empty.");
+ }
+ // automatically dispose all the tracked values in the current scope.
+ const currScope = this.autoDisposeScope.pop() as Array<Disposable |
undefined>;
+ for (let i = 0; i < currScope.length; ++i) {
+ const val = currScope[i];
+ if (val !== undefined) {
+ val.dispose();
+ }
+ }
+ }
+
+ /**
+ * Track object for dispose in current scope.
+ *
+ * @param obj The object to be tracked.
+ * @returns the same object.
+ * @note This function only needs to be called for raw system C API values.
+ * The return value of PackedFunc will be automatically tracked.
+ */
+ attachToCurrentScope<T extends Disposable>(obj: T): T {
+ if (this.autoDisposeScope.length == 0) {
+ throw Error("Must call beginScope to use functions that returns TVM
objects");
+ }
+ const currScope = this.autoDisposeScope[this.autoDisposeScope.length - 1];
+ currScope.push(obj);
+ return obj;
+ }
+
+ moveToParentScope<T extends Disposable>(obj: T): T {
+ this.detachFromCurrentScope(obj);
+ if (this.autoDisposeScope.length < 2) {
+ throw Error("moveToParentScope: Parent scope do not exist");
+ }
+ const parentScope = this.autoDisposeScope[this.autoDisposeScope.length -
2];
+ parentScope.push(obj);
+ return obj;
+ }
+
+ detachFromCurrentScope<T extends Disposable>(obj: T): T {
+ const currScope = this.autoDisposeScope[this.autoDisposeScope.length - 1];
+ let occurance = 0;
+ for (let i = 0; i < currScope.length; ++i) {
+ if (currScope[i] === obj) {
+ occurance += 1;
+ currScope[i] = undefined;
+ }
+ }
+ if (occurance == 0) {
+ throw Error("Cannot find obj in the current auto conversion pool");
+ }
+ if (occurance > 1) {
+ throw Error("Value attached to scope multiple times");
+ }
+ return obj;
+ }
+}
+
/**
* A typed scalar constant used to represent a typed number
* argument to PackedFunc calls.
@@ -154,7 +244,7 @@ export class Scalar {
* Cell holds the PackedFunc object.
*/
class PackedFuncCell implements Disposable {
- handle: Pointer;
+ private handle: Pointer;
private lib: FFILibrary;
constructor(handle: Pointer, lib: FFILibrary) {
@@ -170,6 +260,13 @@ class PackedFuncCell implements Disposable {
this.handle = 0;
}
}
+
+ getHandle(requireNotNull : boolean = true): Pointer {
+ if (requireNotNull && this.handle == 0) {
+ throw Error("PackedFunc has already been disposed");
+ }
+ return this.handle;
+ }
}
const DeviceEnumToStr: Record<number, string> = {
@@ -286,7 +383,7 @@ export class DLDataType {
*/
export class NDArray implements Disposable {
/** Internal array handle. */
- handle: Pointer;
+ private handle: Pointer;
/** Number of dimensions. */
ndim: number;
/** Data type of the array. */
@@ -352,6 +449,19 @@ export class NDArray implements Disposable {
this.byteOffset = lib.memory.loadI64(this.dltensor +
arrayOffsetByteOffset);
}
+ /**
+ * Get handle of ndarray, check it is not null.
+ *
+ * @param requireNotNull require handle is not null.
+ * @returns The handle.
+ */
+ getHandle(requireNotNull : boolean = true): Pointer {
+ if (requireNotNull && this.handle == 0) {
+ throw Error("NDArray has already been disposed");
+ }
+ return this.handle;
+ }
+
dispose(): void {
if (this.handle != 0 && !this.isView) {
this.lib.checkCall(
@@ -371,8 +481,8 @@ export class NDArray implements Disposable {
if (data instanceof NDArray) {
this.lib.checkCall(
(this.lib.exports.TVMArrayCopyFromTo as ctypes.FTVMArrayCopyFromTo)(
- data.handle,
- this.handle,
+ data.getHandle(),
+ this.getHandle(),
0
)
);
@@ -427,7 +537,7 @@ export class NDArray implements Disposable {
this.lib.memory.storeRawBytes(tempPtr, data);
this.lib.checkCall(
(this.lib.exports.TVMArrayCopyFromBytes as
ctypes.FTVMArrayCopyFromBytes)(
- this.handle,
+ this.getHandle(),
tempPtr,
nbytes
)
@@ -455,7 +565,7 @@ export class NDArray implements Disposable {
const tempPtr = stack.ptrFromOffset(tempOffset);
this.lib.checkCall(
(this.lib.exports.TVMArrayCopyToBytes as ctypes.FTVMArrayCopyToBytes)(
- this.handle,
+ this.getHandle(),
tempPtr,
nbytes
)
@@ -499,7 +609,7 @@ export class NDArray implements Disposable {
* Runtime Module.
*/
export class Module implements Disposable {
- handle: Pointer;
+ private handle: Pointer;
private lib: FFILibrary;
private makePackedFunc: (ptr: Pointer) => PackedFunc;
@@ -522,12 +632,28 @@ export class Module implements Disposable {
}
}
+ /**
+ * Get handle of module, check it is not null.
+ *
+ * @param requireNotNull require handle is not null.
+ * @returns The handle.
+ */
+ getHandle(requireNotNull : boolean = true): Pointer {
+ if (requireNotNull && this.handle == 0) {
+ throw Error("Module has already been disposed");
+ }
+ return this.handle;
+ }
+
/**
* Get a function in the module.
* @param name The name of the function.
* @returns The result function.
*/
getFunction(name: string): PackedFunc {
+ if (this.handle == 0) {
+ throw Error("Module has already been disposed");
+ }
const stack = this.lib.getOrAllocCallStack();
const nameOffset = stack.allocRawBytes(name.length + 1);
stack.storeRawBytes(nameOffset, StringToUint8Array(name));
@@ -539,7 +665,7 @@ export class Module implements Disposable {
this.lib.checkCall(
(this.lib.exports.TVMModGetFunction as ctypes.FTVMModGetFunction)(
- this.handle,
+ this.getHandle(),
stack.ptrFromOffset(nameOffset),
1,
outPtr
@@ -561,112 +687,122 @@ export class Module implements Disposable {
importModule(mod: Module): void {
this.lib.checkCall(
(this.lib.exports.TVMModImport as ctypes.FTVMModImport)(
- this.handle,
- mod.handle
+ this.getHandle(),
+ mod.getHandle()
)
);
}
}
/**
- * Graph executor.
- *
- * This is a thin wrapper of the underlying TVM module.
- * you can also directly call set_input, run, and get_output
- * of underlying module functions
+ * Generic object base
*/
-class GraphExecutor implements Disposable {
- module: Module;
- private packedSetInput: PackedFunc;
- private packedRun: PackedFunc;
- private packedGetOutput: PackedFunc;
- private packedLoadParams: PackedFunc;
+ export class TVMObject implements Disposable {
+ private handle: Pointer;
+ private lib: FFILibrary;
+ protected ctx: RuntimeContext;
- /**
- * COnstructor
- * @param module The underlying module.
- */
- constructor(module: Module) {
- this.module = module;
- this.packedSetInput = module.getFunction("set_input");
- this.packedRun = module.getFunction("run");
- this.packedGetOutput = module.getFunction("get_output");
- this.packedLoadParams = module.getFunction("load_params");
+ constructor(
+ handle: Pointer,
+ lib: FFILibrary,
+ ctx: RuntimeContext
+ ) {
+ this.handle = handle;
+ this.lib = lib;
+ this.ctx = ctx;
}
dispose(): void {
- this.packedSetInput.dispose();
- this.packedRun.dispose();
- this.packedGetOutput.dispose();
+ if (this.handle != 0) {
+ this.lib.checkCall(
+ (this.lib.exports.TVMObjectFree as ctypes.FTVMObjectFree)(this.handle)
+ );
+ this.handle = 0;
+ }
}
/**
- * Set input to the executor.
+ * Get handle of module, check it is not null.
*
- * @param key The input key.
- * @param value The value to get set.
+ * @param requireNotNull require handle is not null.
+ * @returns The handle.
*/
- setInput(key: number | string, value: NDArray): void {
- if (typeof key == "number") {
- this.packedSetInput(new Scalar(key, "int32"), value);
- } else {
- this.packedSetInput(key, value);
+ getHandle(requireNotNull : boolean = true): Pointer {
+ if (requireNotNull && this.handle == 0) {
+ throw Error("Module has already been disposed");
+ }
+ return this.handle;
+ }
+ /** get the type index of the object */
+ typeIndex(): number {
+ if (this.handle == 0) {
+ throw Error("The current Object has already been disposed");
}
+ const stack = this.lib.getOrAllocCallStack();
+ const outOffset = stack.allocPtrArray(1);
+ const outPtr = stack.ptrFromOffset(outOffset);
+
+ this.lib.checkCall(
+ (this.lib.exports.TVMObjectGetTypeIndex as
ctypes.FTVMObjectGetTypeIndex)(
+ this.getHandle(),
+ outPtr
+ )
+ );
+ const result = this.lib.memory.loadU32(outPtr);
+ this.lib.recycleCallStack(stack);
+ return result;
}
- /**
- * Execute the underlying graph.
- */
- run(): void {
- this.packedRun();
+ /** get the type key of the object */
+ typeKey(): string {
+ const type_index = this.typeIndex();
+ const stack = this.lib.getOrAllocCallStack();
+ const outOffset = stack.allocPtrArray(1);
+ const outPtr = stack.ptrFromOffset(outOffset);
+ this.lib.checkCall(
+ (this.lib.exports.TVMObjectTypeIndex2Key as
ctypes.FTVMObjectTypeIndex2Key)(
+ type_index,
+ outPtr
+ )
+ );
+ const result =this.lib.memory.loadCString(
+ this.lib.memory.loadPointer(outPtr)
+ );
+ this.lib.recycleCallStack(stack);
+ return result;
}
+}
- /**
- * Get index-th output.
- * @param index The index number.
- * @param out The optional output storage parameters.
- * @returns The output array.
- */
- getOutput(index: number, out: NDArray | undefined = undefined): NDArray {
- if (out !== undefined) {
- this.packedGetOutput(new Scalar(index, "int32"), out)
- return out;
- } else {
- return this.packedGetOutput(new Scalar(index, "int32"));
- }
+/** Objectconstructor */
+type FObjectConstructor = (handle: Pointer, lib: FFILibrary, ctx:
RuntimeContext) => TVMObject;
+
+/** All possible object types. */
+type TVMObjectBase = TVMObject | NDArray | Module | PackedFunc;
+
+/** Runtime array object. */
+export class TVMArray extends TVMObject {
+ constructor(
+ handle: Pointer,
+ lib: FFILibrary,
+ ctx: RuntimeContext
+ ) {
+ super(handle, lib, ctx);
}
/**
- * Load parameters from parameter binary.
- * @param paramBinary The parameter binary.
+ * @returns the size of the array.
*/
- loadParams(paramBinary: Uint8Array): void {
- this.packedLoadParams(paramBinary);
+ size() : number {
+ return this.ctx.arrayGetSize(this) as number;
}
-
/**
- * Benchmark stable execution of the graph(without data copy).
- * @params dev The device to sync during each run.
- * @number The number of times to compute the average.
- * @repeat The number of times to repeat the run.
+ * Get index-th element of the array
+ * @param index the array index.
+ * @returns The element.
*/
- async benchmarkRuns(dev: DLDevice, number=10, repeat=4): Promise<number[]> {
- // Skip first run as it can involve GPU warmup and module loading time.
- const perf = compact.getPerformance();
- const results = [];
- this.run();
- await dev.sync();
- for (let k = 0; k < repeat; ++k) {
- const tstart = perf.now();
- for (let i = 0; i < number; ++i) {
- this.run();
- }
- await dev.sync();
- const tend = perf.now();
- results.push((tend - tstart) / number);
- }
- return results;
+ get(index : number) : TVMObjectBase {
+ return this.ctx.arrayGetItem(this, new Scalar(index, "int32")) as
TVMObjectBase;
}
}
@@ -678,12 +814,28 @@ const enum AyncCallbackCode {
/**
* TVM runtime instance.
+ *
+ * All objects(NDArray, Module, PackedFunc) returned by TVM runtim function
call
+ * and PackedFunc instance are tracked through a scope mechanism that will get
+ * auto-released when we call EndScope.
+ *
+ * This is necessarily to be able to release the underlying WASM and WebGPU
memory that
+ * are not tracked through JS native garbage collection mechanism.
+ *
+ * This does mean that we have to get familar with the following functions:
+ * - {@link beginScope}
+ * - {@link endScope}
+ * - {@link withNewScope}
+ * - {@link attachToCurrentScope}
+ * - {@link detachFromCurrentScope}
*/
export class Instance implements Disposable {
memory: Memory;
exports: Record<string, Function>;
private lib: FFILibrary;
private env: Environment;
+ private objFactory: Map<number, FObjectConstructor>;
+ private ctx: RuntimeContext;
/**
* Internal function(registered by the runtime)
@@ -726,22 +878,136 @@ export class Instance implements Disposable {
this.lib = new FFILibrary(wasmInstance, env.imports);
this.memory = this.lib.memory;
this.exports = this.lib.exports;
+ this.objFactory = new Map<number, ObjectConstructor>();
+ this.ctx = new RuntimeContext(
+ (name: string) => {
+ const autoAttachToScope = false;
+ // runtime context function do not auto-release.
+ return this.getGlobalFuncInternal(name, autoAttachToScope);
+ }
+ );
this.registerEnvGlobalPackedFuncs();
+ this.registerObjectFactoryFuncs();
}
+ /**
+ * Benchmark stable execution of the run function.
+ *
+ * @params run The run function
+ * @params dev The device to sync during each run.
+ * @number The number of times to compute the average.
+ * @repeat The number of times to repeat the run.
+ */
+ async benchmark(run: ()=>void, dev: DLDevice, number=10, repeat=4):
Promise<number[]> {
+ // Skip first run as it can involve GPU warmup and module loading time.
+ const perf = compact.getPerformance();
+ const results = [];
+
+ // run with new scope
+ this.withNewScope(run);
+ await dev.sync();
+
+ for (let k = 0; k < repeat; ++k) {
+ const tstart = perf.now();
+ for (let i = 0; i < number; ++i) {
+ this.withNewScope(run);
+ }
+ await dev.sync();
+ const tend = perf.now();
+ results.push((tend - tstart) / number);
+ }
+ return results;
+ }
+
dispose(): void {
+ // order matters
+ // ctx release goes back into lib.
+ this.ctx.dispose();
this.lib.dispose();
}
+
+ /**
+ * Begin a new scope for tracking object disposal.
+ */
+ beginScope(): void {
+ this.ctx.beginScope();
+ }
+
+ /**
+ * End a scope and release all created TVM objects
+ * under the current scope.
+ *
+ * Exception: one can call retainToParentScope to move
+ * a value to parent scope.
+ */
+ endScope(): void {
+ this.ctx.endScope();
+ }
+
+ /**
+ * Perform action under a new scope.
+ *
+ * @param action The action function.
+ * @returns The result value.
+ *
+ * @note For action to return a valid value,
+ * we will need to call {@link retainToParentScope}
+ * for the objects that are created in the scope.
+ */
+ withNewScope<T>(action: ()=>T): T {
+ this.beginScope();
+ const val = action();
+ this.endScope();
+ return val;
+ }
+
+ /**
+ * Attach a detached obj to the auto-release pool of the current scope.
+ *
+ * @param obj The input obj.
+ * @note Normally user do not need to call this function explicitly, as
+ * all library call return values are explicitly attached to
+ * the current scope. You only need to do so when you call
+ * {@link detachFromCurrentScope} to create a detached object.
+ */
+ attachToCurrentScope<T extends Disposable>(obj: T) : T {
+ return this.ctx.attachToCurrentScope(obj);
+ }
+
+ /**
+ * Move obj's attachment to the parent scope.
+ *
+ * This function is useful to make sure objects are still
+ * alive when exit the current scope.
+ *
+ * @param obj The object to be moved.
+ * @returns The input obj.
+ */
+ moveToParentScope<T extends Disposable>(obj: T) : T {
+ return this.ctx.moveToParentScope(obj);
+ }
+
+ /**
+ * Detach the object from the current scope
+ * so it won't be released via auto-release during endscope.
+ *
+ * User needs to either explicitly call obj.dispose(), or
+ * {@link attachToCurrentScope} to re-attach to the current scope.
+ *
+ * This function can be used to return values to the parent scope.
+ * @param obj The object.
+ */
+ detachFromCurrentScope<T extends Disposable>(obj: T): T {
+ return this.ctx.detachFromCurrentScope(obj);
+ }
+
/**
* Get system-wide library module in the wasm.
* System lib is a global module that contains self register functions in
startup.
* @returns The system library module.
*/
systemLib(): Module {
- const getSysLib = this.getGlobalFunc("runtime.SystemLib");
- const mod = getSysLib() as Module;
- getSysLib.dispose();
- return mod;
+ return this.ctx.getSysLib() as Module;
}
/**
* List all the global function names registered in the runtime.
@@ -791,29 +1057,39 @@ export class Instance implements Disposable {
func: PackedFunc | Function,
override = false
): void {
- const packedFunc = this.toPackedFunc(func);
- const ioverride = override ? 1 : 0;
+ this.withNewScope(() => {
+ const autoAttachToScope = true;
+ // packed func can be released once it is registered
+ const packedFunc = this.toPackedFuncInternal(func, autoAttachToScope);
+ const ioverride = override ? 1 : 0;
- const stack = this.lib.getOrAllocCallStack();
- const nameOffset = stack.allocRawBytes(name.length + 1);
- stack.storeRawBytes(nameOffset, StringToUint8Array(name));
- stack.commitToWasmMemory();
+ const stack = this.lib.getOrAllocCallStack();
+ const nameOffset = stack.allocRawBytes(name.length + 1);
+ stack.storeRawBytes(nameOffset, StringToUint8Array(name));
+ stack.commitToWasmMemory();
- this.lib.checkCall(
- (this.lib.exports.TVMFuncRegisterGlobal as
ctypes.FTVMFuncRegisterGlobal)(
- stack.ptrFromOffset(nameOffset),
- packedFunc._tvmPackedCell.handle,
- ioverride
- )
- );
+ this.lib.checkCall(
+ (this.lib.exports.TVMFuncRegisterGlobal as
ctypes.FTVMFuncRegisterGlobal)(
+ stack.ptrFromOffset(nameOffset),
+ packedFunc._tvmPackedCell.getHandle(),
+ ioverride
+ )
+ );
+ this.lib.recycleCallStack(stack);
+ });
}
/**
* Get global PackedFunc from the runtime.
* @param name The name of the function.
+ * @param autoAttachToScope Whether to track it via autoDispose
* @returns The result function.
*/
getGlobalFunc(name: string): PackedFunc {
+ return this.getGlobalFuncInternal(name, true);
+ }
+
+ private getGlobalFuncInternal(name: string, autoAttachToScope: boolean =
true): PackedFunc {
const stack = this.lib.getOrAllocCallStack();
const nameOffset = stack.allocRawBytes(name.length + 1);
stack.storeRawBytes(nameOffset, StringToUint8Array(name));
@@ -834,6 +1110,7 @@ export class Instance implements Disposable {
throw Error("Cannot find global function " + name);
}
const ret = this.makePackedFunc(handle);
+ if (autoAttachToScope) this.ctx.attachToCurrentScope(ret);
return ret;
}
@@ -854,9 +1131,15 @@ export class Instance implements Disposable {
* @param func Input function.
* @returns The converted function.
*/
- toPackedFunc(func: Function): PackedFunc {
+ toPackedFunc(func: Function): PackedFunc {
+ return this.toPackedFuncInternal(func, true);
+ }
+
+ private toPackedFuncInternal(func: Function, autoAttachToScope: boolean):
PackedFunc {
if (this.isPackedFunc(func)) return func as PackedFunc;
- return this.createPackedFuncFromCFunc(this.wrapJSFuncAsPackedCFunc(func));
+ const ret =
this.createPackedFuncFromCFunc(this.wrapJSFuncAsPackedCFunc(func));
+ if (autoAttachToScope) return this.ctx.attachToCurrentScope(ret);
+ return ret;
}
/**
@@ -979,29 +1262,74 @@ export class Instance implements Disposable {
outPtr
)
);
- const ret = new NDArray(this.memory.loadPointer(outPtr), false, this.lib);
+ const ret = this.ctx.attachToCurrentScope(
+ new NDArray(this.memory.loadPointer(outPtr), false, this.lib)
+ );
this.lib.recycleCallStack(stack);
return ret;
}
/**
- * Create a new graph executor.
+ * Create an tuple {@link TVMArray} input array.
+ *
+ * The input array can be passed to tvm runtime function
+ * and needs to b explicitly disposed.
*
- * @param graphJson The graph executor json file.
- * @param lib The underlying library.
- * @param dev The execution device of the graph.
+ * @param inputs The input array
+ * @returns The result array.
*/
- createGraphExecutor(graphJson: string, lib: Module, dev: DLDevice):
GraphExecutor {
- const fcreate = this.getGlobalFunc('tvm.graph_executor.create');
- const module = fcreate(
- graphJson,
- lib,
- this.scalar(dev.deviceType, "int32"),
- this.scalar(dev.deviceId, "int32")) as Module;
- return new GraphExecutor(module);
+ makeTVMArray(
+ inputs: Array<TVMObjectBase>
+ ): TVMArray {
+ return this.ctx.arrayMake(...inputs) as TVMArray;
}
+ /**
+ * Get type index from type key.
+ * @param typeKey The type key.
+ * @returns The corresponding type index.
+ */
+ typeKey2Index(
+ typeKey: string
+ ) : number {
+ const stack = this.lib.getOrAllocCallStack();
+ const typeKeyOffset = stack.allocRawBytes(typeKey.length + 1);
+ stack.storeRawBytes(typeKeyOffset, StringToUint8Array(typeKey));
+ const outOffset = stack.allocPtrArray(1);
+ const outPtr = stack.ptrFromOffset(outOffset);
+
+ stack.commitToWasmMemory(outOffset);
+
+ this.lib.checkCall(
+ (this.lib.exports.TVMObjectTypeKey2Index as
ctypes.FTVMObjectTypeKey2Index)(
+ stack.ptrFromOffset(typeKeyOffset),
+ outPtr
+ )
+ );
+ const typeIndex = this.memory.loadU32(outPtr);
+ this.lib.recycleCallStack(stack);
+ return typeIndex;
+ }
+ /**
+ * Register an object constructor.
+ * @param typeKey The name of the function.
+ * @param func function to be registered.
+ * @param override Whether overwrite function in existing registry.
+ */
+ registerObjectConstructor(
+ typeKey: string,
+ func: FObjectConstructor,
+ override = false
+ ): void {
+ const typeIndex = this.typeKey2Index(typeKey);
+ if (this.objFactory.has(typeIndex)) {
+ if (!override) {
+ throw new Error("Type " + typeKey + " already registered");
+ }
+ }
+ this.objFactory.set(typeIndex, func);
+ }
/**
* Register an asyncfunction to be global function in the server.
* @param name The name of the function.
@@ -1017,10 +1345,12 @@ export class Instance implements Disposable {
): void {
const asyncVariant = (...args: Array<any>): void => {
const fargs = args.slice(0, args.length - 1);
- const callback = args[args.length - 1] as PackedFunc;
+ // need to keep it alive until callback is fulfilled.
+ const callback = this.detachFromCurrentScope(args[args.length - 1] as
PackedFunc);
const promise: Promise<any> = func(...fargs);
promise.then((rv: any) => {
callback(this.scalar(AyncCallbackCode.kReturn, "int32"), rv);
+ callback.dispose();
});
};
this.registerFunc("__async." + name, asyncVariant, override);
@@ -1046,6 +1376,14 @@ export class Instance implements Disposable {
this.lib.webGPUContext = webGPUContext;
}
+ /** Register all object factory */
+ private registerObjectFactoryFuncs(): void {
+ this.registerObjectConstructor("Array",
+ (handle: number, lib: FFILibrary, ctx: RuntimeContext) => {
+ return new TVMArray(handle, lib, ctx);
+ });
+ }
+
/** Register global packed functions needed by the backend to the env. */
private registerEnvGlobalPackedFuncs(): void {
// Register the timer function to enable the time_evaluator.
@@ -1062,6 +1400,11 @@ export class Instance implements Disposable {
cooldownIntervalMs: number,
repeatsToCooldown: number
): Promise<Uint8Array> => {
+ // detach and explicit dispose when tasks is fullfilled
+ // the promise will immediately return and we need to makesure
+ // finvoke do not get recycled.
+ this.ctx.detachFromCurrentScope(finvoke);
+
finvoke(this.scalar(1, "int32"));
await dev.sync();
const result = [];
@@ -1095,6 +1438,9 @@ export class Instance implements Disposable {
}
const ret = new Float64Array(result.length);
ret.set(result);
+
+ // dispose finvoke
+ finvoke.dispose();
return new Uint8Array(ret.buffer);
};
@@ -1154,7 +1500,7 @@ export class Instance implements Disposable {
const valueOffset = argsValue + i * SizeOf.TVMValue;
const codeOffset = argsCode + i * SizeOf.I32;
if (val instanceof NDArray) {
- stack.storePtr(valueOffset, val.handle);
+ stack.storePtr(valueOffset, val.getHandle());
stack.storeI32(codeOffset, ArgTypeCode.TVMNDArrayHandle);
} else if (val instanceof Scalar) {
if (val.dtype.startsWith("int") || val.dtype.startsWith("uint")) {
@@ -1177,7 +1523,7 @@ export class Instance implements Disposable {
stack.storeI32(codeOffset, ArgTypeCode.Float);
// eslint-disable-next-line no-prototype-builtins
} else if (tp == "function" && val.hasOwnProperty("_tvmPackedCell")) {
- stack.storePtr(valueOffset, val._tvmPackedCell.handle);
+ stack.storePtr(valueOffset, val._tvmPackedCell.getHandle());
stack.storeI32(codeOffset, ArgTypeCode.TVMPackedFuncHandle);
} else if (val === null || val == undefined) {
stack.storePtr(valueOffset, 0);
@@ -1189,13 +1535,16 @@ export class Instance implements Disposable {
stack.allocThenSetArgBytes(valueOffset, val);
stack.storeI32(codeOffset, ArgTypeCode.TVMBytes);
} else if (val instanceof Function) {
- val = this.toPackedFunc(val);
+ val = this.toPackedFuncInternal(val, false);
stack.tempArgs.push(val);
- stack.storePtr(valueOffset, val._tvmPackedCell.handle);
+ stack.storePtr(valueOffset, val._tvmPackedCell.getHandle());
stack.storeI32(codeOffset, ArgTypeCode.TVMPackedFuncHandle);
} else if (val instanceof Module) {
- stack.storePtr(valueOffset, val.handle);
+ stack.storePtr(valueOffset, val.getHandle());
stack.storeI32(codeOffset, ArgTypeCode.TVMModuleHandle);
+ } else if (val instanceof TVMObject) {
+ stack.storePtr(valueOffset, val.getHandle());
+ stack.storeI32(codeOffset, ArgTypeCode.TVMObjectHandle);
} else {
throw new Error("Unsupported argument type " + tp);
}
@@ -1213,6 +1562,8 @@ export class Instance implements Disposable {
_handle: Pointer
): number => {
const jsArgs = [];
+ // use scope to track js values.
+ this.ctx.beginScope();
for (let i = 0; i < nargs; ++i) {
const valuePtr = argValues + i * SizeOf.TVMValue;
const codePtr = argCodes + i * SizeOf.I32;
@@ -1237,6 +1588,8 @@ export class Instance implements Disposable {
}
const rv = func(...jsArgs);
+ // recycle all js object value in function unless we want to retain them.
+ this.ctx.endScope();
if (rv !== undefined && rv !== null) {
const stack = lib.getOrAllocCallStack();
@@ -1281,7 +1634,7 @@ export class Instance implements Disposable {
this.lib.checkCall(
(this.exports.TVMFuncCall as ctypes.FTVMFuncCall)(
- handle,
+ cell.getHandle(),
stack.ptrFromOffset(valueOffset),
stack.ptrFromOffset(tcodeOffset),
args.length,
@@ -1304,6 +1657,13 @@ export class Instance implements Disposable {
return ret as PackedFunc;
}
+ /**
+ * Creaye return value of the packed func. The value us auto-tracked for
dispose.
+ * @param rvaluePtr The location of rvalue
+ * @param tcode The type code.
+ * @param callbackArg Whether it is being used in callbackArg.
+ * @returns The JS value.
+ */
private retValueToJS(rvaluePtr: Pointer, tcode: number, callbackArg:
boolean): any {
switch (tcode) {
case ArgTypeCode.Int:
@@ -1315,23 +1675,45 @@ export class Instance implements Disposable {
return this.memory.loadPointer(rvaluePtr);
}
case ArgTypeCode.TVMNDArrayHandle: {
- return new NDArray(this.memory.loadPointer(rvaluePtr), false,
this.lib);
+ return this.ctx.attachToCurrentScope(
+ new NDArray(this.memory.loadPointer(rvaluePtr), false, this.lib)
+ );
}
case ArgTypeCode.TVMDLTensorHandle: {
assert(callbackArg);
+ // no need to attach as we are only looking at view
return new NDArray(this.memory.loadPointer(rvaluePtr), true, this.lib);
}
case ArgTypeCode.TVMPackedFuncHandle: {
- return this.makePackedFunc(this.memory.loadPointer(rvaluePtr));
+ return this.ctx.attachToCurrentScope(
+ this.makePackedFunc(this.memory.loadPointer(rvaluePtr))
+ );
}
case ArgTypeCode.TVMModuleHandle: {
- return new Module(
+ return this.ctx.attachToCurrentScope(
+ new Module(
+ this.memory.loadPointer(rvaluePtr),
+ this.lib,
+ (ptr: Pointer) => {
+ return this.ctx.attachToCurrentScope(this.makePackedFunc(ptr));
+ }
+ )
+ );
+ }
+ case ArgTypeCode.TVMObjectHandle: {
+ const obj = new TVMObject(
this.memory.loadPointer(rvaluePtr),
this.lib,
- (ptr: Pointer) => {
- return this.makePackedFunc(ptr);
- }
+ this.ctx
);
+ const func = this.objFactory.get(obj.typeIndex())
+ if (func != undefined) {
+ return this.ctx.attachToCurrentScope(
+ func(obj.getHandle(), this.lib, this.ctx)
+ );
+ } else {
+ return this.ctx.attachToCurrentScope(obj);
+ }
}
case ArgTypeCode.Null: return undefined;
case ArgTypeCode.DLDevice: {
diff --git a/web/tests/node/test_module_load.js
b/web/tests/node/test_module_load.js
index 561de8aa57..24acc66a1e 100644
--- a/web/tests/node/test_module_load.js
+++ b/web/tests/node/test_module_load.js
@@ -32,8 +32,6 @@ const tvm = new tvmjs.Instance(
new EmccWASI()
);
-// Load system library
-const sysLib = tvm.systemLib();
function randomArray(length, max) {
return Array.apply(null, Array(length)).map(function () {
@@ -42,8 +40,13 @@ function randomArray(length, max) {
}
test("add one", () => {
+ tvm.beginScope();
+ // Load system library
+ const sysLib = tvm.systemLib();
// grab pre-loaded function
const faddOne = sysLib.getFunction("add_one");
+ tvm.detachFromCurrentScope(faddOne);
+
assert(tvm.isPackedFunc(faddOne));
const n = 124;
const A = tvm.empty(n).copyFrom(randomArray(n, 1));
@@ -56,5 +59,13 @@ test("add one", () => {
for (var i = 0; i < BB.length; ++i) {
assert(Math.abs(BB[i] - (AA[i] + 1)) < 1e-5);
}
+ tvm.endScope();
+
+ // assert auto release scope behavior
+ assert(sysLib.getHandle(false) == 0);
+ // fadd is not released because it is detached
+ assert(faddOne._tvmPackedCell.handle != 0);
faddOne.dispose();
+ assert(A.getHandle(false) == 0);
+ assert(B.getHandle(false) == 0);
});
diff --git a/web/tests/node/test_ndarray.js b/web/tests/node/test_ndarray.js
index b7a5abdcb1..8393c668dd 100644
--- a/web/tests/node/test_ndarray.js
+++ b/web/tests/node/test_ndarray.js
@@ -28,6 +28,7 @@ const wasmSource = fs.readFileSync(path.join(wasmPath,
"tvmjs_runtime.wasm"));
let tvm = new tvmjs.Instance(new WebAssembly.Module(wasmSource), new
EmccWASI());
+
// Basic fields.
assert(tvm.listGlobalFuncNames() !== undefined);
@@ -42,15 +43,14 @@ function testArrayCopy(dtype, arrayType) {
let ret = a.toArray();
assert(ret instanceof arrayType);
assert(ret.toString() == arrayType.from(data).toString());
- // test multiple dispose.
- a.dispose();
- a.dispose();
}
test("array copy", () => {
- testArrayCopy("float32", Float32Array);
- testArrayCopy("int", Int32Array);
- testArrayCopy("int8", Int8Array);
- testArrayCopy("uint8", Uint8Array);
- testArrayCopy("float64", Float64Array);
+ tvm.withNewScope(() => {
+ testArrayCopy("float32", Float32Array);
+ testArrayCopy("int", Int32Array);
+ testArrayCopy("int8", Int8Array);
+ testArrayCopy("uint8", Uint8Array);
+ testArrayCopy("float64", Float64Array);
+ });
});
diff --git a/web/tests/node/test_ndarray.js b/web/tests/node/test_object.js
similarity index 64%
copy from web/tests/node/test_ndarray.js
copy to web/tests/node/test_object.js
index b7a5abdcb1..3b7ee5bd0c 100644
--- a/web/tests/node/test_ndarray.js
+++ b/web/tests/node/test_object.js
@@ -28,29 +28,18 @@ const wasmSource = fs.readFileSync(path.join(wasmPath,
"tvmjs_runtime.wasm"));
let tvm = new tvmjs.Instance(new WebAssembly.Module(wasmSource), new
EmccWASI());
-// Basic fields.
-assert(tvm.listGlobalFuncNames() !== undefined);
+test("object", () => {
+ tvm.withNewScope(() => {
+ let data = [1, 2, 3, 4, 5, 6];
+ let a = tvm.empty([2, 3], "float32").copyFrom(data);
-// Test ndarray
-function testArrayCopy(dtype, arrayType) {
- let data = [1, 2, 3, 4, 5, 6];
- let a = tvm.empty([2, 3], dtype).copyFrom(data);
+ let t = tvm.makeTVMArray([]);
+ let b = tvm.makeTVMArray([a, t]);
+ // assert b instanceof tvmjs.TVMArray
+ assert(b instanceof tvmjs.TVMArray);
+ assert(b.size() == 2);
- assert(a.device.toString() == "cpu(0)");
- assert(a.shape[0] == 2 && a.shape[1] == 3);
-
- let ret = a.toArray();
- assert(ret instanceof arrayType);
- assert(ret.toString() == arrayType.from(data).toString());
- // test multiple dispose.
- a.dispose();
- a.dispose();
-}
-
-test("array copy", () => {
- testArrayCopy("float32", Float32Array);
- testArrayCopy("int", Int32Array);
- testArrayCopy("int8", Int8Array);
- testArrayCopy("uint8", Uint8Array);
- testArrayCopy("float64", Float64Array);
+ let t1 = b.get(1);
+ assert(t1.getHandle() == t.getHandle());
+ });
});
diff --git a/web/tests/node/test_packed_func.js
b/web/tests/node/test_packed_func.js
index 6e0546f39d..98956ebf2b 100644
--- a/web/tests/node/test_packed_func.js
+++ b/web/tests/node/test_packed_func.js
@@ -31,7 +31,9 @@ let tvm = new tvmjs.Instance(
new EmccWASI()
);
+
test("GetGlobal", () => {
+ tvm.beginScope();
let flist = tvm.listGlobalFuncNames();
let faddOne = tvm.getGlobalFunc("testing.add_one");
let fecho = tvm.getGlobalFunc("testing.echo");
@@ -51,25 +53,35 @@ test("GetGlobal", () => {
assert(fecho(undefined) == undefined);
+ tvm.beginScope();
+
let arr = tvm.empty([2, 2]).copyFrom([1, 2, 3, 4]);
let arr2 = fecho(arr);
- assert(arr.handle == arr2.handle);
+ assert(arr.getHandle() == arr2.getHandle());
assert(arr2.toArray().toString() == arr.toArray().toString());
+ tvm.moveToParentScope(arr2);
+ tvm.endScope();
+ // test move to parent scope and tracking
+ assert(arr.getHandle(false) == 0);
+ assert(arr2.handle != 0);
+
let mod = tvm.systemLib();
let ret = fecho(mod);
- assert(ret.handle == mod.handle);
+ assert(ret.getHandle() == mod.getHandle());
assert(flist.length != 0);
-
- mod.dispose();
- ret.dispose();
- arr.dispose();
- arr2.dispose();
- fecho.dispose();
- faddOne.dispose();
+ tvm.endScope();
+
+ // assert auto release scope behavior
+ assert(mod.getHandle(false) == 0);
+ assert(ret.getHandle(false) == 0);
+ assert(arr2.getHandle(false) == 0);
+ assert(fecho._tvmPackedCell.getHandle(false) == 0);
+ assert(faddOne._tvmPackedCell.getHandle(false) == 0);
});
test("ReturnFunc", () => {
+ tvm.beginScope();
function addy(y) {
function add(x, z) {
return x + y + z;
@@ -95,9 +107,11 @@ test("ReturnFunc", () => {
// test multiple dispose.
f.dispose();
f.dispose();
+ tvm.endScope();
});
test("RegisterGlobal", () => {
+ tvm.beginScope();
tvm.registerFunc("xyz", function (x, y) {
return x + y;
});
@@ -108,23 +122,44 @@ test("RegisterGlobal", () => {
let syslib = tvm.systemLib();
syslib.dispose();
+ tvm.endScope();
});
test("NDArrayCbArg", () => {
+ tvm.beginScope();
let use_count = tvm.getGlobalFunc("testing.object_use_count");
+ let record = [];
- let fcheck = tvm.toPackedFunc(function (x) {
+ let fcheck = tvm.toPackedFunc(function (x, retain) {
assert(use_count(x) == 2);
- x.dispose();
+ assert(x.handle != 0);
+ record.push(x);
+ if (retain) {
+ tvm.detachFromCurrentScope(x);
+ }
});
+
let x = tvm.empty([2], "float32").copyFrom([1, 2]);
assert(use_count(x) == 1);
- fcheck(x);
+
+ fcheck(x, 0);
+ // auto-released when it is out of scope.
+ assert(record[0].getHandle(false) == 0);
+
assert(use_count(x) == 1);
+
+ fcheck(x, 1);
+ assert(use_count(x) == 2);
+ assert(record[1].handle != 0);
+ tvm.attachToCurrentScope(record[1]);
+ tvm.endScope();
+ assert(record[1].getHandle(false) == 0);
});
test("Logging", () => {
+ tvm.beginScope();
const log_info = tvm.getGlobalFunc("testing.log_info_str");
log_info("helow world")
log_info.dispose();
+ tvm.endScope();
});
diff --git a/web/tests/python/websock_rpc_test.py
b/web/tests/python/websock_rpc_test.py
index 9aab1759f8..7de5ee956e 100644
--- a/web/tests/python/websock_rpc_test.py
+++ b/web/tests/python/websock_rpc_test.py
@@ -69,7 +69,6 @@ def test_rpc():
assert fecho(100, 2, 3) == 100
assert fecho("xyz") == "xyz"
assert bytes(fecho(bytearray(b"123"))) == b"123"
-
# run the generated library.
f1 = remote.system_lib()
dev = remote.cpu(0)