Merge pull request #220 from ThakeeNathees/leak-detect

Memory leak detection implemented
This commit is contained in:
Thakee Nathees 2022-05-03 19:25:31 +05:30 committed by GitHub
commit a9f57d9ec7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 33 deletions

75
scripts/leak_detect.py Normal file
View File

@ -0,0 +1,75 @@
##
## Copyright (c) 2020-2022 Thakee Nathees
## Copyright (c) 2021-2022 Pocketlang Contributors
## Distributed Under The MIT License
##
## A quick script to detect memory leaks, from the trace report.
## To get the trace report redefine TRACE_MEMORY as 1 at the
## pk_internal.h and compile pocketlang.
import sys
def detect_leak():
trace_log_id = "[memory trace]"
total_bytes = 0 ## Totally allocated bytes.
mem = dict() ## key = address, value = bytes.
trace_log_file_path = sys.argv[1]
with open(trace_log_file_path, 'r') as f:
for line in f.readlines():
if line.startswith(trace_log_id):
line = line[len(trace_log_id) + 1:].strip()
type, data = split_and_strip(line, ':')
addr, bytes = split_and_strip(data, '=')
bytes = bytes.replace(' bytes', '')
if type == "malloc":
bytes = int(bytes)
assert(bytes >= 0); total_bytes += bytes
mem[addr] = bytes
elif type == "free":
bytes = int(bytes)
assert(bytes <= 0); total_bytes += bytes
mem.pop(addr)
elif type == "realloc":
oldp, newp = split_and_strip(addr, '->')
olds, news = split_and_strip(bytes, '->')
olds = int(olds); news = int(news)
total_bytes += (news - olds)
assert(mem[oldp] == olds)
mem.pop(oldp)
mem[newp] = news
success = True
if total_bytes != 0:
print_err(f"Memory leak detected - {total_bytes} bytes were never freed.")
success = False
if len(mem) != 0:
print_err("Memory leak detected - some addresses were never freed.")
for addr in mem:
print(f" {addr} : {mem[addr]} bytes")
success = False
if success:
print("No leaks were detected.")
else:
sys.exit(1)
def split_and_strip(string, delim):
return map(lambda s: s.strip(), string.split(delim))
def print_err(msg):
print("Error:", msg, file=sys.stderr)
if __name__ == "__main__":
detect_leak()

View File

@ -179,7 +179,7 @@ static void _reportStackFrame(PKVM* vm, CallFrame* frame) {
// reducing it by 1. But stack overflows are occure before executing // reducing it by 1. But stack overflows are occure before executing
// any instruction of that function, so the instruction_index possibly // any instruction of that function, so the instruction_index possibly
// be -1 (set it to zero in that case). // be -1 (set it to zero in that case).
int instruction_index = frame->ip - fn->fn->opcodes.data - 1; int instruction_index = (int) (frame->ip - fn->fn->opcodes.data) - 1;
if (instruction_index == -1) instruction_index++; if (instruction_index == -1) instruction_index++;
int line = fn->fn->oplines.data[instruction_index]; int line = fn->fn->oplines.data[instruction_index];

View File

@ -50,6 +50,10 @@
// Dump the stack values and the globals. // Dump the stack values and the globals.
#define DUMP_STACK 0 #define DUMP_STACK 0
// Trace memory allocations. Enable this and redirect the trace dump to a file
// and run the script leak_detect.py to check for memory leaks.
#define TRACE_MEMORY 0
// Nan-Tagging could be disable for debugging/portability purposes. See "var.h" // Nan-Tagging could be disable for debugging/portability purposes. See "var.h"
// header for more information on Nan-tagging. // header for more information on Nan-tagging.
#define VAR_NAN_TAGGING 1 #define VAR_NAN_TAGGING 1
@ -91,8 +95,18 @@
((type*)vmRealloc(vm, NULL, 0, sizeof(type) * (count))) ((type*)vmRealloc(vm, NULL, 0, sizeof(type) * (count)))
// Deallocate a pointer allocated by vmRealloc before. // Deallocate a pointer allocated by vmRealloc before.
#define DEALLOCATE(vm, pointer) \ #define DEALLOCATE(vm, pointer, type) \
vmRealloc(vm, pointer, 0, 0) vmRealloc(vm, pointer, sizeof(type), 0)
// Deallocate object of [type] which has a dynamic tail array of type
// [tail_type] with [count] entries.
#define DEALLOCATE_DYNAMIC(vm, pointer, type, count, tail_type) \
((type*)vmRealloc(vm, pointer, \
sizeof(type) + sizeof(tail_type) * (count), 0))
// Deallocate [count] amount of object of [type] array.
#define DEALLOCATE_ARRAY(vm, pointer, type, count) \
((type*)vmRealloc(vm, pointer, sizeof(type) * (count), 0))
/*****************************************************************************/ /*****************************************************************************/
/* REUSABLE INTERNAL MACROS */ /* REUSABLE INTERNAL MACROS */

View File

@ -129,7 +129,7 @@ void pkFreeVM(PKVM* vm) {
// before freeing the VM. // before freeing the VM.
ASSERT(vm->handles == NULL, "Not all handles were released."); ASSERT(vm->handles == NULL, "Not all handles were released.");
DEALLOCATE(vm, vm); DEALLOCATE(vm, vm, PKVM);
} }
void* pkGetUserData(const PKVM* vm) { void* pkGetUserData(const PKVM* vm) {
@ -247,7 +247,7 @@ void pkReleaseHandle(PKVM* vm, PkHandle* handle) {
if (handle->prev) handle->prev->next = handle->next; if (handle->prev) handle->prev->next = handle->next;
// Free the handle. // Free the handle.
DEALLOCATE(vm, handle); DEALLOCATE(vm, handle, PkHandle);
} }
PkResult pkCompileModule(PKVM* vm, PkHandle* module_handle, PkStringPtr source, PkResult pkCompileModule(PKVM* vm, PkHandle* module_handle, PkStringPtr source,

View File

@ -847,7 +847,7 @@ static void _mapResize(PKVM* vm, Map* self, uint32_t capacity) {
_mapInsertEntry(self, old_entries[i].key, old_entries[i].value); _mapInsertEntry(self, old_entries[i].key, old_entries[i].value);
} }
DEALLOCATE(vm, old_entries); DEALLOCATE_ARRAY(vm, old_entries, MapEntry, old_capacity);
} }
Var mapGet(Map* self, Var key) { Var mapGet(Map* self, Var key) {
@ -871,7 +871,7 @@ void mapSet(PKVM* vm, Map* self, Var key, Var value) {
} }
void mapClear(PKVM* vm, Map* self) { void mapClear(PKVM* vm, Map* self) {
DEALLOCATE(vm, self->entries); DEALLOCATE_ARRAY(vm, self->entries, MapEntry, self->capacity);
self->entries = NULL; self->entries = NULL;
self->capacity = 0; self->capacity = 0;
self->count = 0; self->count = 0;
@ -930,60 +930,87 @@ void freeObject(PKVM* vm, Object* self) {
// will won't be freed here instead they haven't marked at all, and will be // will won't be freed here instead they haven't marked at all, and will be
// removed at the sweeping phase of the garbage collection. // removed at the sweeping phase of the garbage collection.
switch (self->type) { switch (self->type) {
case OBJ_STRING: case OBJ_STRING: {
break; String* str = (String*) self;
DEALLOCATE_DYNAMIC(vm, str, String, str->capacity, char);
return;
};
case OBJ_LIST: case OBJ_LIST: {
pkVarBufferClear(&(((List*)self)->elements), vm); pkVarBufferClear(&(((List*)self)->elements), vm);
break; DEALLOCATE(vm, self, List);
return;
}
case OBJ_MAP: case OBJ_MAP: {
DEALLOCATE(vm, ((Map*)self)->entries); Map* map = (Map*)self;
break; DEALLOCATE_ARRAY(vm, map->entries, MapEntry, map->capacity);
DEALLOCATE(vm, self, Map);
return;
}
case OBJ_RANGE: case OBJ_RANGE: {
break; DEALLOCATE(vm, self, Range);
return;
}
case OBJ_MODULE: { case OBJ_MODULE: {
Module* module = (Module*)self; Module* module = (Module*)self;
pkVarBufferClear(&module->globals, vm); pkVarBufferClear(&module->globals, vm);
pkUintBufferClear(&module->global_names, vm); pkUintBufferClear(&module->global_names, vm);
pkVarBufferClear(&module->constants, vm); pkVarBufferClear(&module->constants, vm);
} break; DEALLOCATE(vm, self, Module);
return;
}
case OBJ_FUNC: { case OBJ_FUNC: {
Function* func = (Function*)self; Function* func = (Function*)self;
if (!func->is_native) { if (!func->is_native) {
pkByteBufferClear(&func->fn->opcodes, vm); pkByteBufferClear(&func->fn->opcodes, vm);
pkUintBufferClear(&func->fn->oplines, vm); pkUintBufferClear(&func->fn->oplines, vm);
DEALLOCATE(vm, func->fn); DEALLOCATE(vm, func->fn, Fn);
} }
} break; DEALLOCATE(vm, self, Function);
return;
};
case OBJ_CLOSURE: case OBJ_CLOSURE: {
case OBJ_UPVALUE: DEALLOCATE_DYNAMIC(vm, self, Closure,
break; ((Closure*)self)->fn->upvalue_count, Upvalue*);
return;
}
case OBJ_UPVALUE: {
DEALLOCATE(vm, self, Upvalue);
return;
}
case OBJ_FIBER: { case OBJ_FIBER: {
Fiber* fiber = (Fiber*)self; Fiber* fiber = (Fiber*)self;
DEALLOCATE(vm, fiber->stack); DEALLOCATE_ARRAY(vm, fiber->stack, Var, fiber->stack_size);
DEALLOCATE(vm, fiber->frames); DEALLOCATE_ARRAY(vm, fiber->frames, CallFrame, fiber->frame_capacity);
} break; DEALLOCATE(vm, fiber, Fiber);
return;
}
case OBJ_CLASS: { case OBJ_CLASS: {
Class* cls = (Class*)self; Class* cls = (Class*)self;
pkClosureBufferClear(&cls->methods, vm); pkClosureBufferClear(&cls->methods, vm);
} break; DEALLOCATE(vm, cls, Class);
return;
}
case OBJ_INST: { case OBJ_INST: {
Instance* inst = (Instance*)self; Instance* inst = (Instance*)self;
if (inst->cls->delete_fn != NULL) { if (inst->cls->delete_fn != NULL) {
inst->cls->delete_fn(inst->native); inst->cls->delete_fn(inst->native);
} }
} break; DEALLOCATE(vm, inst, Instance);
return;
}
} }
DEALLOCATE(vm, self); UNREACHABLE();
} }
uint32_t moduleAddConstant(PKVM* vm, Module* module, Var value) { uint32_t moduleAddConstant(PKVM* vm, Module* module, Var value) {

View File

@ -22,8 +22,6 @@ PkHandle* vmNewHandle(PKVM* vm, Var value) {
void* vmRealloc(PKVM* vm, void* memory, size_t old_size, size_t new_size) { void* vmRealloc(PKVM* vm, void* memory, size_t old_size, size_t new_size) {
// TODO: Debug trace allocations here.
// Track the total allocated memory of the VM to trigger the GC. // Track the total allocated memory of the VM to trigger the GC.
// if vmRealloc is called for freeing, the old_size would be 0 since // if vmRealloc is called for freeing, the old_size would be 0 since
// deallocated bytes are traced by garbage collector. // deallocated bytes are traced by garbage collector.
@ -33,7 +31,39 @@ void* vmRealloc(PKVM* vm, void* memory, size_t old_size, size_t new_size) {
vmCollectGarbage(vm); vmCollectGarbage(vm);
} }
#if TRACE_MEMORY
void* ptr = vm->config.realloc_fn(memory, new_size, vm->config.user_data);
do {
// Deallocation of the VM itself cannot be traced.
if (memory == vm) break;
if (memory == NULL && new_size == 0) { //< Just nothing.
ASSERT(old_size == 0, OOPS);
break;
}
if (old_size == 0 && new_size > 0) { // New allocation.
ASSERT(memory == NULL, OOPS);
printf("[memory trace] malloc : %p = %+li bytes\n",
ptr, (long) new_size);
} else if (new_size == 0) { // Free.
ASSERT(memory != NULL && old_size != 0, OOPS);
printf("[memory trace] free : %p = -%li bytes\n",
memory, (long) old_size);
} else { // Realloc.
ASSERT(old_size != 0 && new_size != 0 && memory != NULL, OOPS);
printf("[memory trace] realloc : %p -> %p = %+li -> %+li bytes\n",
memory, ptr, (long) (old_size), (long) (new_size));
}
} while (false);
return ptr;
#else
return vm->config.realloc_fn(memory, new_size, vm->config.user_data); return vm->config.realloc_fn(memory, new_size, vm->config.user_data);
#endif
} }
void vmPushTempRef(PKVM* vm, Object* obj) { void vmPushTempRef(PKVM* vm, Object* obj) {