From: divverent Date: Tue, 15 Jul 2008 07:55:04 +0000 (+0000) Subject: prvm_leaktest - leak detector for QC objects (all but Gecko instances at the moment) X-Git-Tag: xonotic-v0.1.0preview~2177 X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=5aba57f18381d282c8a1be1dd5784bd94ec9f7f2;p=xonotic%2Fdarkplaces.git prvm_leaktest - leak detector for QC objects (all but Gecko instances at the moment) For entities, it uses a marking approach git-svn-id: svn://svn.icculus.org/twilight/trunk/darkplaces@8403 d7cf8633-e32d-0410-b094-e92efae38249 --- diff --git a/progs.h b/progs.h index 5993d20d..d5e59dbf 100644 --- a/progs.h +++ b/progs.h @@ -32,6 +32,10 @@ typedef struct edict_engineprivate_s // sv.time when the object was freed (to prevent early reuse which could // mess up client interpolation or obscure severe QuakeC bugs) float freetime; + // mark for the leak detector + qboolean marked; + // place in the code where it was allocated (for the leak detector) + const char *allocation_origin; // initially false to prevent projectiles from moving on their first frame // (even if they were spawned by an synchronous client think) qboolean move; diff --git a/progsvm.h b/progsvm.h index cf200b19..ce900af7 100644 --- a/progsvm.h +++ b/progsvm.h @@ -67,6 +67,8 @@ typedef struct prvm_edict_private_s { qboolean free; float freetime; + qboolean marked; + const char *allocation_origin; } prvm_edict_private_t; typedef struct prvm_edict_s @@ -297,6 +299,7 @@ typedef struct prvm_stringbuffer_s int max_strings; int num_strings; char **strings; + const char *origin; } prvm_stringbuffer_t; @@ -332,6 +335,7 @@ typedef struct prvm_prog_s int firstfreeknownstring; const char **knownstrings; unsigned char *knownstrings_freeable; + const char **knownstrings_origin; const char ***stringshash; memexpandablearray_t stringbuffersarray; @@ -365,7 +369,9 @@ typedef struct prvm_prog_s // until this point everything also exists (with the pr_ prefix) in the old vm qfile_t *openfiles[PRVM_MAX_OPENFILES]; + const char * openfiles_origin[PRVM_MAX_OPENFILES]; fssearch_t *opensearches[PRVM_MAX_OPENSEARCHES]; + const char * opensearches_origin[PRVM_MAX_OPENSEARCHES]; struct clgecko_s *opengeckoinstances[PRVM_MAX_GECKOINSTANCES]; // copies of some vars that were former read from sv @@ -405,6 +411,7 @@ typedef struct prvm_prog_s // used to indicate whether a prog is loaded qboolean loaded; + qboolean leaktest_active; // prvm_builtin_mem_t *mem_list; @@ -491,6 +498,8 @@ void PRVM_PrintFunction_f (void); void PRVM_PrintState(void); void PRVM_CrashAll (void); void PRVM_Crash (void); +void PRVM_ShortStackTrace(char *buf, size_t bufsize); +const char *PRVM_AllocationOrigin(); ddef_t *PRVM_ED_FindField(const char *name); ddef_t *PRVM_ED_FindGlobal(const char *name); diff --git a/prvm_cmds.c b/prvm_cmds.c index 7ab9143e..2bf5a2fa 100644 --- a/prvm_cmds.c +++ b/prvm_cmds.c @@ -1619,6 +1619,7 @@ void VM_fopen(void) PRVM_G_FLOAT(OFS_RETURN) = filenum; if (developer.integer >= 100) Con_Printf("VM_fopen: %s: %s mode %s opened as #%i\n", PRVM_NAME, filename, modestring, filenum); + prog->openfiles_origin[filenum] = PRVM_AllocationOrigin(); } } @@ -1649,6 +1650,8 @@ void VM_fclose(void) } FS_Close(prog->openfiles[filenum]); prog->openfiles[filenum] = NULL; + if(prog->openfiles_origin[filenum]) + PRVM_Free((char *)prog->openfiles_origin[filenum]); if (developer.integer >= 100) Con_Printf("VM_fclose: %s: #%i closed\n", PRVM_NAME, filenum); } @@ -2627,7 +2630,10 @@ void VM_search_begin(void) if(!(prog->opensearches[handle] = FS_Search(pattern,caseinsens, quiet))) PRVM_G_FLOAT(OFS_RETURN) = -1; else + { + prog->opensearches_origin[handle] = PRVM_AllocationOrigin(); PRVM_G_FLOAT(OFS_RETURN) = handle; + } } /* @@ -2657,6 +2663,8 @@ void VM_search_end(void) FS_FreeSearch(prog->opensearches[handle]); prog->opensearches[handle] = NULL; + if(prog->opensearches_origin[handle]) + PRVM_Free((char *)prog->opensearches_origin[handle]); } /* @@ -3893,6 +3901,7 @@ void VM_buf_create (void) VM_SAFEPARMCOUNT(0, VM_buf_create); stringbuffer = Mem_ExpandableArray_AllocRecord(&prog->stringbuffersarray); for (i = 0;stringbuffer != Mem_ExpandableArray_RecordAtIndex(&prog->stringbuffersarray, i);i++); + stringbuffer->origin = PRVM_AllocationOrigin(); PRVM_G_FLOAT(OFS_RETURN) = i; } @@ -3916,6 +3925,8 @@ void VM_buf_del (void) Mem_Free(stringbuffer->strings[i]); if (stringbuffer->strings) Mem_Free(stringbuffer->strings); + if(stringbuffer->origin) + PRVM_Free((char *)stringbuffer->origin); Mem_ExpandableArray_FreeRecord(&prog->stringbuffersarray, stringbuffer); } else diff --git a/prvm_edict.c b/prvm_edict.c index 658b01c8..204fb3bd 100644 --- a/prvm_edict.c +++ b/prvm_edict.c @@ -40,6 +40,8 @@ cvar_t prvm_traceqc = {0, "prvm_traceqc", "0", "prints every QuakeC statement as // LordHavoc: counts usage of each QuakeC statement cvar_t prvm_statementprofiling = {0, "prvm_statementprofiling", "0", "counts how many times each QuakeC statement has been executed, these counts are displayed in prvm_printfunction output (if enabled)"}; cvar_t prvm_backtraceforwarnings = {0, "prvm_backtraceforwarnings", "0", "print a backtrace for warnings too"}; +cvar_t prvm_leaktest = {0, "prvm_leaktest", "0", "try to detect memory leaks in strings or entities"}; +cvar_t prvm_leaktest_ignore_classnames = {0, "prvm_leaktest_ignore", "", "classnames of entities to NOT leak check because they are found by find(world, classname, ...) but are actually spawned by QC code (NOT map entities)"}; extern sizebuf_t vm_tempstringsbuf; @@ -219,6 +221,18 @@ void PRVM_ED_ClearEdict (prvm_edict_t *e) PRVM_GCALL(init_edict)(e); } +const char *PRVM_AllocationOrigin() +{ + char *buf = NULL; + if(prog->leaktest_active) + if(prog->depth > 0) // actually in QC code and not just parsing the entities block of a map/savegame + { + buf = (char *)PRVM_Alloc(128); + PRVM_ShortStackTrace(buf, 128); + } + return buf; +} + /* ================= PRVM_ED_Alloc @@ -248,6 +262,7 @@ prvm_edict_t *PRVM_ED_Alloc (void) if (e->priv.required->free && ( e->priv.required->freetime < 2 || prog->globaloffsets.time < 0 || (PRVM_GLOBALFIELDVALUE(prog->globaloffsets.time)->_float - e->priv.required->freetime) > 0.5 ) ) { PRVM_ED_ClearEdict (e); + e->priv.required->allocation_origin = PRVM_AllocationOrigin(); return e; } } @@ -262,6 +277,8 @@ prvm_edict_t *PRVM_ED_Alloc (void) e = PRVM_EDICT_NUM(i); PRVM_ED_ClearEdict (e); + e->priv.required->allocation_origin = PRVM_AllocationOrigin(); + return e; } @@ -283,6 +300,11 @@ void PRVM_ED_Free (prvm_edict_t *ed) ed->priv.required->free = true; ed->priv.required->freetime = prog->globaloffsets.time >= 0 ? PRVM_GLOBALFIELDVALUE(prog->globaloffsets.time)->_float : 0; + if(ed->priv.required->allocation_origin) + { + PRVM_Free((char *)ed->priv.required->allocation_origin); + ed->priv.required->allocation_origin = NULL; + } } //=========================================================================== @@ -1518,8 +1540,10 @@ PRVM_ResetProg =============== */ +void PRVM_LeakTest(); void PRVM_ResetProg() { + PRVM_LeakTest(); PRVM_GCALL(reset_cmd)(); Mem_FreePool(&prog->progs_mempool); memset(prog,0,sizeof(prvm_prog_t)); @@ -2071,6 +2095,7 @@ void PRVM_Init (void) Cvar_RegisterVariable (&prvm_traceqc); Cvar_RegisterVariable (&prvm_statementprofiling); Cvar_RegisterVariable (&prvm_backtraceforwarnings); + Cvar_RegisterVariable (&prvm_leaktest); //VM_Cmd_Init(); } @@ -2094,6 +2119,7 @@ void PRVM_InitProg(int prognr) prog->starttime = Sys_DoubleTime(); prog->error_cmd = Host_Error; + prog->leaktest_active = prvm_leaktest.integer; } int PRVM_GetProgNr() @@ -2259,19 +2285,27 @@ int PRVM_SetEngineString(const char *s) { const char **oldstrings = prog->knownstrings; const unsigned char *oldstrings_freeable = prog->knownstrings_freeable; + const char **oldstrings_origin = prog->knownstrings_origin; prog->maxknownstrings += 128; prog->knownstrings = (const char **)PRVM_Alloc(prog->maxknownstrings * sizeof(char *)); prog->knownstrings_freeable = (unsigned char *)PRVM_Alloc(prog->maxknownstrings * sizeof(unsigned char)); + if(prog->leaktest_active) + prog->knownstrings_origin = (const char **)PRVM_Alloc(prog->maxknownstrings * sizeof(char *)); if (prog->numknownstrings) { memcpy((char **)prog->knownstrings, oldstrings, prog->numknownstrings * sizeof(char *)); memcpy((char **)prog->knownstrings_freeable, oldstrings_freeable, prog->numknownstrings * sizeof(unsigned char)); + if(prog->leaktest_active) + memcpy((char **)prog->knownstrings_origin, oldstrings_origin, prog->numknownstrings * sizeof(char *)); } } prog->numknownstrings++; } prog->firstfreeknownstring = i + 1; prog->knownstrings[i] = s; + prog->knownstrings_freeable[i] = false; + if(prog->leaktest_active) + prog->knownstrings_origin[i] = NULL; return -1 - i; } @@ -2332,20 +2366,28 @@ int PRVM_AllocString(size_t bufferlength, char **pointer) { const char **oldstrings = prog->knownstrings; const unsigned char *oldstrings_freeable = prog->knownstrings_freeable; + const char **oldstrings_origin = prog->knownstrings_origin; prog->maxknownstrings += 128; prog->knownstrings = (const char **)PRVM_Alloc(prog->maxknownstrings * sizeof(char *)); prog->knownstrings_freeable = (unsigned char *)PRVM_Alloc(prog->maxknownstrings * sizeof(unsigned char)); + if(prog->leaktest_active) + prog->knownstrings_origin = (const char **)PRVM_Alloc(prog->maxknownstrings * sizeof(char *)); if (prog->numknownstrings) { memcpy((char **)prog->knownstrings, oldstrings, prog->numknownstrings * sizeof(char *)); memcpy((char **)prog->knownstrings_freeable, oldstrings_freeable, prog->numknownstrings * sizeof(unsigned char)); + if(prog->leaktest_active) + memcpy((char **)prog->knownstrings_origin, oldstrings_origin, prog->numknownstrings * sizeof(char *)); } + // TODO why not Mem_Free the old ones? } prog->numknownstrings++; } prog->firstfreeknownstring = i + 1; prog->knownstrings[i] = (char *)PRVM_Alloc(bufferlength); prog->knownstrings_freeable[i] = true; + if(prog->leaktest_active) + prog->knownstrings_origin[i] = PRVM_AllocationOrigin(); if (pointer) *pointer = (char *)(prog->knownstrings[i]); return -1 - i; @@ -2362,9 +2404,12 @@ void PRVM_FreeString(int num) num = -1 - num; if (!prog->knownstrings[num]) PRVM_ERROR("PRVM_FreeString: attempt to free a non-existent or already freed string"); - if (!prog->knownstrings[num]) + if (!prog->knownstrings_freeable[num]) PRVM_ERROR("PRVM_FreeString: attempt to free a string owned by the engine"); PRVM_Free((char *)prog->knownstrings[num]); + if(prog->leaktest_active) + if(prog->knownstrings_origin[num]) + PRVM_Free((char *)prog->knownstrings_origin[num]); prog->knownstrings[num] = NULL; prog->knownstrings_freeable[num] = false; prog->firstfreeknownstring = min(prog->firstfreeknownstring, num); @@ -2373,3 +2418,244 @@ void PRVM_FreeString(int num) PRVM_ERROR("PRVM_FreeString: invalid string offset %i", num); } +static qboolean PRVM_IsStringReferenced(string_t string) +{ + int i, j; + + for (i = 0;i < prog->progs->numglobaldefs;i++) + { + ddef_t *d = &prog->globaldefs[i]; + if((etype_t)((int) d->type & ~DEF_SAVEGLOBAL) != ev_string) + continue; + if(string == ((prvm_eval_t *) &prog->globals.generic[d->ofs])->string) + return true; + } + + for(j = 0; j < prog->num_edicts; ++j) + { + prvm_edict_t *ed = PRVM_EDICT_NUM(j); + if (ed->priv.required->free) + continue; + for (i=0; iprogs->numfielddefs; ++i) + { + ddef_t *d = &prog->fielddefs[i]; + if((etype_t)((int) d->type & ~DEF_SAVEGLOBAL) != ev_string) + continue; + if(string == ((prvm_eval_t *) &((float*)ed->fields.vp)[d->ofs])->string) + return true; + } + } + + return false; +} + +static qboolean PRVM_IsEdictRelevant(prvm_edict_t *edict) +{ + if(PRVM_NUM_FOR_EDICT(edict) <= prog->reserved_edicts) + return true; // world or clients + switch(prog - prog_list) + { + case PRVM_SERVERPROG: + { + entvars_t *ev = edict->fields.server; + if(ev->solid) // can block other stuff, or is a trigger? + return true; + if(ev->modelindex) // visible ent? + return true; + if(ev->effects) // particle effect? + return true; + if(ev->think) // has a think function? + if(ev->nextthink > 0) // that actually will eventually run? + return true; + if(ev->takedamage) + return true; + if(*prvm_leaktest_ignore_classnames.string) + { + if(strstr(va(" %s ", prvm_leaktest_ignore_classnames.string), va(" %s ", PRVM_GetString(ev->classname)))) + return true; + } + } + break; + case PRVM_CLIENTPROG: + { + // TODO someone add more stuff here + cl_entvars_t *ev = edict->fields.client; + if(ev->modelindex) // visible ent? + return true; + if(ev->effects) // particle effect? + return true; + if(ev->think) // has a think function? + if(ev->nextthink > 0) // that actually will eventually run? + return true; + if(*prvm_leaktest_ignore_classnames.string) + { + if(strstr(va(" %s ", prvm_leaktest_ignore_classnames.string), va(" %s ", PRVM_GetString(ev->classname)))) + return true; + } + } + break; + case PRVM_MENUPROG: + // menu prog does not have classnames + break; + } + return false; +} + +static qboolean PRVM_IsEdictReferenced(prvm_edict_t *edict) +{ + int i, j; + int edictnum = PRVM_NUM_FOR_EDICT(edict); + const char *targetname = NULL; + + switch(prog - prog_list) + { + case PRVM_SERVERPROG: + targetname = PRVM_GetString(edict->fields.server->targetname); + break; + } + + if(targetname) + if(!*targetname) // "" + targetname = NULL; + + for (i = 0;i < prog->progs->numglobaldefs;i++) + { + ddef_t *d = &prog->globaldefs[i]; + if((etype_t)((int) d->type & ~DEF_SAVEGLOBAL) != ev_entity) + continue; + if(edictnum == ((prvm_eval_t *) &prog->globals.generic[d->ofs])->edict) + return true; + } + + for(j = 0; j < prog->num_edicts; ++j) + { + prvm_edict_t *ed = PRVM_EDICT_NUM(j); + if (!ed->priv.required->marked) + continue; + if(ed == edict) + continue; + if(targetname) + { + const char *target = PRVM_GetString(ed->fields.server->target); + if(target) + if(!strcmp(target, targetname)) + return true; + } + for (i=0; iprogs->numfielddefs; ++i) + { + ddef_t *d = &prog->fielddefs[i]; + if((etype_t)((int) d->type & ~DEF_SAVEGLOBAL) != ev_entity) + continue; + if(edictnum == ((prvm_eval_t *) &((float*)ed->fields.vp)[d->ofs])->edict) + return true; + } + } + + return false; +} + +static void PRVM_MarkReferencedEdicts() +{ + int j; + qboolean found_new; + + for(j = 0; j < prog->num_edicts; ++j) + { + prvm_edict_t *ed = PRVM_EDICT_NUM(j); + if(ed->priv.required->free) + continue; + ed->priv.required->marked = PRVM_IsEdictRelevant(ed); + } + + do + { + found_new = false; + for(j = 0; j < prog->num_edicts; ++j) + { + prvm_edict_t *ed = PRVM_EDICT_NUM(j); + if(ed->priv.required->free) + continue; + if(ed->priv.required->marked) + continue; + if(PRVM_IsEdictReferenced(ed)) + { + ed->priv.required->marked = true; + found_new = true; + } + } + } + while(found_new); +} + +void PRVM_LeakTest() +{ + int i, j; + qboolean leaked = false; + + if(!prog->leaktest_active) + return; + + // 1. Strings + for (i = 0; i < prog->numknownstrings; ++i) + { + if(prog->knownstrings[i]) + if(prog->knownstrings_freeable[i]) + if(prog->knownstrings_origin[i]) + if(!PRVM_IsStringReferenced(-1 - i)) + { + Con_Printf("Unreferenced string found!\n Value: %s\n Origin: %s\n", prog->knownstrings[i], prog->knownstrings_origin[i]); + leaked = true; + } + } + + // 2. Edicts + PRVM_MarkReferencedEdicts(); + for(j = 0; j < prog->num_edicts; ++j) + { + prvm_edict_t *ed = PRVM_EDICT_NUM(j); + if(ed->priv.required->free) + continue; + if(!ed->priv.required->marked) + if(ed->priv.required->allocation_origin) + { + Con_Printf("Unreferenced edict found!\n Allocated at: %s\n", ed->priv.required->allocation_origin); + PRVM_ED_Print(ed, NULL); + Con_Print("\n"); + leaked = true; + } + } + + for (i = 0; i < (int)Mem_ExpandableArray_IndexRange(&prog->stringbuffersarray); ++i) + { + prvm_stringbuffer_t *stringbuffer = Mem_ExpandableArray_RecordAtIndex(&prog->stringbuffersarray, i); + if(stringbuffer) + if(stringbuffer->origin) + { + Con_Printf("Open string buffer handle found!\n Allocated at: %s\n", stringbuffer->origin); + leaked = true; + } + } + + for(i = 0; i < PRVM_MAX_OPENFILES; ++i) + { + if(prog->openfiles[i]) + if(prog->openfiles_origin[i]) + { + Con_Printf("Open file handle found!\n Allocated at: %s\n", prog->openfiles_origin[i]); + leaked = true; + } + } + + for(i = 0; i < PRVM_MAX_OPENSEARCHES; ++i) + { + if(prog->opensearches[i]) + if(prog->opensearches_origin[i]) + { + Con_Printf("Open search handle found!\n Allocated at: %s\n", prog->opensearches_origin[i]); + leaked = true; + } + } + + if(!leaked) + Con_Printf("Congratulations. No leaks found.\n"); +} diff --git a/prvm_exec.c b/prvm_exec.c index 95a94196..b866ed27 100644 --- a/prvm_exec.c +++ b/prvm_exec.c @@ -265,6 +265,37 @@ void PRVM_StackTrace (void) } } +void PRVM_ShortStackTrace(char *buf, size_t bufsize) +{ + mfunction_t *f; + int i; + + if(prog) + { + dpsnprintf(buf, bufsize, "(%s) ", prog->name); + } + else + { + strlcpy(buf, "", sizeof(buf)); + return; + } + + prog->stack[prog->depth].s = prog->xstatement; + prog->stack[prog->depth].f = prog->xfunction; + for (i = prog->depth;i > 0;i--) + { + f = prog->stack[i].f; + + if(strlcat(buf, + f + ? va("%s:%s(%i) ", PRVM_GetString(f->s_file), PRVM_GetString(f->s_name), prog->stack[i].s - f->first_statement) + : " ", + bufsize + ) >= bufsize) + break; + } +} + void PRVM_CallProfile () {