From 73225b4ef7910b9ab000e01eda97ddba317e64cd Mon Sep 17 00:00:00 2001 From: Brendan Eich Date: Tue, 27 Jan 2009 19:56:55 -0800 Subject: [PATCH] Bug 454184 - Implement eval caching (r=mrbkap). --- js/src/jsapi.cpp | 4 ++ js/src/jsapi.h | 6 ++- js/src/jscntxt.cpp | 53 +++++++++++++++++-- js/src/jscntxt.h | 62 +++++++++++++++++----- js/src/jsgc.cpp | 6 ++- js/src/jsobj.cpp | 122 ++++++++++++++++++++++++++++++++++++++------ js/src/jsopcode.cpp | 1 + js/src/jsparse.cpp | 45 ++++++++++++---- js/src/jsparse.h | 3 +- js/src/jsscript.h | 1 + 10 files changed, 257 insertions(+), 46 deletions(-) diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 0efa4773007f..ee72277676c8 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -1183,6 +1183,10 @@ JS_GetOptions(JSContext *cx) (cx)->version |= JSVERSION_HAS_XML; \ else \ (cx)->version &= ~JSVERSION_HAS_XML; \ + if ((cx)->options & JSOPTION_ANONFUNFIX) \ + (cx)->version |= JSVERSION_ANONFUNFIX; \ + else \ + (cx)->version &= ~JSVERSION_ANONFUNFIX; \ JS_END_MACRO JS_PUBLIC_API(uint32) diff --git a/js/src/jsapi.h b/js/src/jsapi.h index dddda3db82b3..5552f5ab89bc 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -596,8 +596,10 @@ JS_StringToVersion(const char *string); will be passed to each call to JS_ExecuteScript. */ #define JSOPTION_UNROOTED_GLOBAL JS_BIT(13) /* The GC will not root the - global objects leaving - that up to the embedding. */ + contexts' global objects + (see JS_GetGlobalObject), + leaving that up to the + embedding. */ extern JS_PUBLIC_API(uint32) JS_GetOptions(JSContext *cx); diff --git a/js/src/jscntxt.cpp b/js/src/jscntxt.cpp index fd33df29233d..2f4e6c4eebe6 100644 --- a/js/src/jscntxt.cpp +++ b/js/src/jscntxt.cpp @@ -168,7 +168,7 @@ js_GetCurrentThread(JSRuntime *rt) memset(&thread->traceMonitor, 0, sizeof(thread->traceMonitor)); js_InitJIT(&thread->traceMonitor); #endif - thread->scriptsToGC = NULL; + memset(thread->scriptsToGC, 0, sizeof thread->scriptsToGC); /* * js_SetContextThread initializes the remaining fields as necessary. @@ -197,8 +197,11 @@ js_SetContextThread(JSContext *cx) * current thread. See bug 425828. */ if (JS_CLIST_IS_EMPTY(&thread->contextList)) { - memset(&thread->gsnCache, 0, sizeof(thread->gsnCache)); - memset(&thread->propertyCache, 0, sizeof(thread->propertyCache)); + memset(&thread->gsnCache, 0, sizeof thread->gsnCache); + memset(&thread->propertyCache, 0, sizeof thread->propertyCache); +#ifdef DEBUG + memset(&thread->evalCacheMeter, 0, sizeof thread->evalCacheMeter); +#endif } /* Assert that the previous cx->thread called JS_ClearContextThread(). */ @@ -350,6 +353,49 @@ js_NewContext(JSRuntime *rt, size_t stackChunkSize) return cx; } +#if defined DEBUG && defined XP_UNIX +# include + +static void +DumpEvalCacheMeter(JSContext *cx) +{ + struct { + const char *name; + ptrdiff_t offset; + } table[] = { +#define frob(x) { #x, offsetof(JSEvalCacheMeter, x) } + EVAL_CACHE_METER_LIST(frob) +#undef frob + }; + JSEvalCacheMeter *ecm = &JS_CACHE_LOCUS(cx)->evalCacheMeter; + + static FILE *fp; + if (!fp) { + fp = fopen("/tmp/evalcache.stats", "w"); + if (!fp) + return; + } + + fprintf(fp, "eval cache meter (%p):\n", +#ifdef JS_THREADSAFE + cx->thread +#else + cx->runtime +#endif + ); + for (uintN i = 0; i < JS_ARRAY_LENGTH(table); ++i) { + fprintf(fp, "%-8.8s %llu\n", + table[i].name, *(uint64 *)((uint8 *)ecm + table[i].offset)); + } + fprintf(fp, "hit ratio %g%%\n", ecm->hit * 100. / ecm->probe); + fprintf(fp, "avg steps %g\n", double(ecm->step) / ecm->probe); + fflush(fp); +} +# define DUMP_EVAL_CACHE_METER(cx) DumpEvalCacheMeter(cx) +#else +# define DUMP_EVAL_CACHE_METER(cx) ((void) 0) +#endif + void js_DestroyContext(JSContext *cx, JSDestroyContextMode mode) { @@ -438,6 +484,7 @@ js_DestroyContext(JSContext *cx, JSDestroyContextMode mode) if (last) { js_GC(cx, GC_LAST_CONTEXT); + DUMP_EVAL_CACHE_METER(cx); /* * Free the script filename table if it exists and is empty. Do this diff --git a/js/src/jscntxt.h b/js/src/jscntxt.h index 22f19194b1fb..f0c7d83af7ab 100644 --- a/js/src/jscntxt.h +++ b/js/src/jscntxt.h @@ -159,6 +159,31 @@ typedef struct JSTraceMonitor { # define JS_EXECUTING_TRACE(cx) JS_FALSE #endif +#ifdef DEBUG +# define JS_EVAL_CACHE_METERING 1 +#endif + +/* Number of potentially reusable scriptsToGC to search for the eval cache. */ +#ifndef JS_EVAL_CACHE_SHIFT +# define JS_EVAL_CACHE_SHIFT 6 +#endif +#define JS_EVAL_CACHE_SIZE JS_BIT(JS_EVAL_CACHE_SHIFT) + +#ifdef JS_EVAL_CACHE_METERING +# define EVAL_CACHE_METER_LIST(_) _(probe), _(hit), _(step), _(noscope) +# define ID(x) x + +/* Have to typedef this for LiveConnect C code, which includes us. */ +typedef struct JSEvalCacheMeter { + uint64 EVAL_CACHE_METER_LIST(ID); +} JSEvalCacheMeter; + +# undef ID +# define DECLARE_EVAL_CACHE_METER JSEvalCacheMeter evalCacheMeter; +#else +# define DECLARE_EVAL_CACHE_METER /* nothing */ +#endif + #ifdef JS_THREADSAFE /* @@ -195,14 +220,13 @@ struct JSThread { JSTraceMonitor traceMonitor; #endif - /* Lock-free list of scripts created by eval to garbage-collect. */ - JSScript *scriptsToGC; + /* Lock-free hashed lists of scripts created by eval to garbage-collect. */ + JSScript *scriptsToGC[JS_EVAL_CACHE_SIZE]; + + DECLARE_EVAL_CACHE_METER }; -#define JS_GSN_CACHE(cx) ((cx)->thread->gsnCache) -#define JS_PROPERTY_CACHE(cx) ((cx)->thread->propertyCache) -#define JS_TRACE_MONITOR(cx) ((cx)->thread->traceMonitor) -#define JS_SCRIPTS_TO_GC(cx) ((cx)->thread->scriptsToGC) +#define JS_CACHE_LOCUS(cx) ((cx)->thread) extern void js_ThreadDestructorCB(void *ptr); @@ -463,13 +487,12 @@ struct JSRuntime { /* Trace-tree JIT recorder/interpreter state. */ JSTraceMonitor traceMonitor; - /* Lock-free list of scripts created by eval to garbage-collect. */ - JSScript *scriptsToGC; + /* Lock-free hashed lists of scripts created by eval to garbage-collect. */ + JSScript *scriptsToGC[JS_EVAL_CACHE_SIZE]; -#define JS_GSN_CACHE(cx) ((cx)->runtime->gsnCache) -#define JS_PROPERTY_CACHE(cx) ((cx)->runtime->propertyCache) -#define JS_TRACE_MONITOR(cx) ((cx)->runtime->traceMonitor) -#define JS_SCRIPTS_TO_GC(cx) ((cx)->runtime->scriptsToGC) + DECLARE_EVAL_CACHE_METER + +#define JS_CACHE_LOCUS(cx) ((cx)->runtime) #endif /* @@ -587,6 +610,19 @@ struct JSRuntime { #endif }; +/* Common macros to access thread-local caches in JSThread or JSRuntime. */ +#define JS_GSN_CACHE(cx) (JS_CACHE_LOCUS(cx)->gsnCache) +#define JS_PROPERTY_CACHE(cx) (JS_CACHE_LOCUS(cx)->propertyCache) +#define JS_TRACE_MONITOR(cx) (JS_CACHE_LOCUS(cx)->traceMonitor) +#define JS_SCRIPTS_TO_GC(cx) (JS_CACHE_LOCUS(cx)->scriptsToGC) + +#ifdef JS_EVAL_CACHE_METERING +# define EVAL_CACHE_METER(x) (JS_CACHE_LOCUS(cx)->evalCacheMeter.x++) +#else +# define EVAL_CACHE_METER(x) ((void) 0) +#endif +#undef DECLARE_EVAL_CACHE_METER + #ifdef DEBUG # define JS_RUNTIME_METER(rt, which) JS_ATOMIC_INCREMENT(&(rt)->which) # define JS_RUNTIME_UNMETER(rt, which) JS_ATOMIC_DECREMENT(&(rt)->which) @@ -1022,6 +1058,8 @@ class JSAutoResolveFlags #define JSVERSION_MASK 0x0FFF /* see JSVersion in jspubtd.h */ #define JSVERSION_HAS_XML 0x1000 /* flag induced by XML option */ +#define JSVERSION_ANONFUNFIX 0x2000 /* see jsapi.h, the comments + for JSOPTION_ANONFUNFIX */ #define JSVERSION_NUMBER(cx) ((JSVersion)((cx)->version & \ JSVERSION_MASK)) diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 31a49bb6b110..51c701d892b8 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -3470,7 +3470,8 @@ js_GC(JSContext *cx, JSGCInvocationKind gckind) #endif /* Destroy eval'ed scripts. */ - DestroyScriptsToGC(cx, &JS_SCRIPTS_TO_GC(cx)); + for (i = 0; i < JS_ARRAY_LENGTH(JS_SCRIPTS_TO_GC(cx)); i++) + DestroyScriptsToGC(cx, &JS_SCRIPTS_TO_GC(cx)[i]); #ifdef JS_THREADSAFE /* @@ -3492,7 +3493,8 @@ js_GC(JSContext *cx, JSGCInvocationKind gckind) #ifdef JS_TRACER js_FlushJITOracle(acx); #endif - DestroyScriptsToGC(cx, &acx->thread->scriptsToGC); + for (i = 0; i < JS_ARRAY_LENGTH(acx->thread->scriptsToGC); i++) + DestroyScriptsToGC(cx, &acx->thread->scriptsToGC[i]); } #else /* The thread-unsafe case just has to clear the runtime's GSN cache. */ diff --git a/js/src/jsobj.cpp b/js/src/jsobj.cpp index 0d06aa54ec26..5360dc62d2e1 100644 --- a/js/src/jsobj.cpp +++ b/js/src/jsobj.cpp @@ -1191,19 +1191,42 @@ js_ComputeFilename(JSContext *cx, JSStackFrame *caller, return caller->script->filename; } +#ifndef EVAL_CACHE_CHAIN_LIMIT +# define EVAL_CACHE_CHAIN_LIMIT 4 +#endif + +static inline JSScript ** +EvalCacheHash(JSContext *cx, JSString *str) +{ + const jschar *s; + size_t n; + uint32 h; + + JSSTRING_CHARS_AND_LENGTH(str, s, n); + if (n > 100) + n = 100; + for (h = 0; n; s++, n--) + h = JS_ROTATE_LEFT32(h, 4) ^ *s; + + h *= JS_GOLDEN_RATIO; + h >>= 32 - JS_EVAL_CACHE_SHIFT; + return &JS_SCRIPTS_TO_GC(cx)[h]; +} + static JSBool obj_eval(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { JSStackFrame *fp, *caller; JSBool indirectCall; JSObject *scopeobj; - JSString *str; + uint32 tcflags; + JSPrincipals *principals; const char *file; uintN line; - JSPrincipals *principals; - uint32 tcflags; + JSString *str; JSScript *script; JSBool ok; + JSScript **bucket = NULL; /* avoid GCC warning with early decl&init */ #if JS_HAS_EVAL_THIS_SCOPE JSObject *callerScopeChain = NULL, *callerVarObj = NULL; JSObject *setCallerScopeChain = NULL; @@ -1336,25 +1359,94 @@ obj_eval(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) goto out; } - str = JSVAL_TO_STRING(argv[0]); + tcflags = TCF_COMPILE_N_GO; if (caller) { + tcflags |= TCF_PUT_STATIC_DEPTH(caller->script->staticDepth + 1); principals = JS_EvalFramePrincipals(cx, fp, caller); file = js_ComputeFilename(cx, caller, principals, &line); } else { + principals = NULL; file = NULL; line = 0; - principals = NULL; } - tcflags = TCF_COMPILE_N_GO; - if (caller) - tcflags |= TCF_PUT_STATIC_DEPTH(caller->script->staticDepth + 1); - script = js_CompileScript(cx, scopeobj, caller, principals, tcflags, - JSSTRING_CHARS(str), JSSTRING_LENGTH(str), - NULL, file, line); + str = JSVAL_TO_STRING(argv[0]); + script = NULL; + + /* Cache local eval scripts indexed by source qualified by scope. */ + bucket = EvalCacheHash(cx, str); + if (caller->fun) { + uintN count = 0; + JSScript **scriptp = bucket; + + EVAL_CACHE_METER(probe); + while ((script = *scriptp) != NULL) { + if ((script->flags & JSSF_SAVED_CALLER_FUN) && + script->version == cx->version && + (script->principals == principals || + (principals->subsume(principals, script->principals) && + script->principals->subsume(script->principals, principals)))) { + /* + * Get the prior (cache-filling) eval's saved caller function. + * See js_CompileScript in jsparse.cpp. + */ + JSFunction *fun; + JS_GET_SCRIPT_FUNCTION(script, 0, fun); + + if (fun == caller->fun) { + /* + * Get the source string passed for safekeeping in the + * atom map by the prior eval to js_CompileScript. + */ + JSString *src = ATOM_TO_STRING(script->atomMap.vector[0]); + + if (src == str || js_EqualStrings(src, str)) { + /* + * Source matches, qualify by comparing scopeobj to the + * COMPILE_N_GO-memoized parent of the first literal + * function or regexp object if any. If none, then this + * script has no compiled-in dependencies on the prior + * eval's scopeobj. + */ + JSObjectArray *objarray = JS_SCRIPT_OBJECTS(script); + int i = 1; + if (objarray->length == 1) { + if (script->regexpsOffset != 0) { + objarray = JS_SCRIPT_REGEXPS(script); + i = 0; + } else { + EVAL_CACHE_METER(noscope); + i = -1; + } + } + if (i < 0 || + STOBJ_GET_PARENT(objarray->vector[i]) == scopeobj) { + EVAL_CACHE_METER(hit); + *scriptp = script->u.nextToGC; + script->u.nextToGC = NULL; + break; + } + } + } + } + + if (++count == EVAL_CACHE_CHAIN_LIMIT) { + script = NULL; + break; + } + EVAL_CACHE_METER(step); + scriptp = &script->u.nextToGC; + } + } + if (!script) { - ok = JS_FALSE; - goto out; + script = js_CompileScript(cx, scopeobj, caller, principals, tcflags, + JSSTRING_CHARS(str), JSSTRING_LENGTH(str), + NULL, file, line, str); + if (!script) { + ok = JS_FALSE; + goto out; + } } if (argc < 2) { @@ -1372,8 +1464,8 @@ obj_eval(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) if (ok) ok = js_Execute(cx, scopeobj, script, caller, JSFRAME_EVAL, rval); - script->u.nextToGC = JS_SCRIPTS_TO_GC(cx); - JS_SCRIPTS_TO_GC(cx) = script; + script->u.nextToGC = *bucket; + *bucket = script; #ifdef CHECK_SCRIPT_OWNER script->owner = NULL; #endif diff --git a/js/src/jsopcode.cpp b/js/src/jsopcode.cpp index bcb56a1fbd9c..2d4148aaa6e9 100644 --- a/js/src/jsopcode.cpp +++ b/js/src/jsopcode.cpp @@ -2752,6 +2752,7 @@ Decompile(SprintStack *ss, jsbytecode *pc, intN nb, JSOp nextop) case JSOP_CALLUPVAR: case JSOP_GETUPVAR: + JS_ASSERT(jp->script->flags & JSSF_SAVED_CALLER_FUN); if (!jp->fun) JS_GET_SCRIPT_FUNCTION(jp->script, 0, jp->fun); diff --git a/js/src/jsparse.cpp b/js/src/jsparse.cpp index 6dfdd6d96def..5addc8f744ed 100644 --- a/js/src/jsparse.cpp +++ b/js/src/jsparse.cpp @@ -503,7 +503,8 @@ extern JSScript * js_CompileScript(JSContext *cx, JSObject *scopeChain, JSStackFrame *callerFrame, JSPrincipals *principals, uint32 tcflags, const jschar *chars, size_t length, - FILE *file, const char *filename, uintN lineno) + FILE *file, const char *filename, uintN lineno, + JSString *source) { JSParseContext pc; JSArenaPool codePool, notePool; @@ -543,16 +544,36 @@ js_CompileScript(JSContext *cx, JSObject *scopeChain, JSStackFrame *callerFrame, cg.treeContext.u.scopeChain = scopeChain; cg.staticDepth = TCF_GET_STATIC_DEPTH(tcflags); - if ((tcflags & TCF_COMPILE_N_GO) && callerFrame && callerFrame->fun) { - /* - * An eval script in a caller frame needs to have its enclosing function - * captured in case it uses an upvar reference, and someone wishes to - * decompile it while running. - */ - JSParsedObjectBox *pob = js_NewParsedObjectBox(cx, &pc, FUN_OBJECT(callerFrame->fun)); - pob->emitLink = cg.objectList.lastPob; - cg.objectList.lastPob = pob; - cg.objectList.length++; + /* + * If funpob is non-null after we create the new script, callerFrame->fun + * was saved in the 0th object table entry. + */ + JSParsedObjectBox *funpob = NULL; + + if (tcflags & TCF_COMPILE_N_GO) { + if (source) { + /* + * Save eval program source in script->atomMap.vector[0] for the + * eval cache (see obj_eval in jsobj.cpp). + */ + JSAtom *atom = js_AtomizeString(cx, source, 0); + if (!atom || !js_IndexAtom(cx, atom, &cg.atomList)) + return NULL; + } + + if (callerFrame && callerFrame->fun) { + /* + * An eval script in a caller frame needs to have its enclosing + * function captured in case it uses an upvar reference, and + * someone wishes to decompile it while it's running. + */ + funpob = js_NewParsedObjectBox(cx, &pc, FUN_OBJECT(callerFrame->fun)); + if (!funpob) + return NULL; + funpob->emitLink = cg.objectList.lastPob; + cg.objectList.lastPob = funpob; + cg.objectList.length++; + } } /* Inline Statements() to emit as we go to save space. */ @@ -647,6 +668,8 @@ js_CompileScript(JSContext *cx, JSObject *scopeChain, JSStackFrame *callerFrame, JS_DumpArenaStats(stdout); #endif script = js_NewScriptFromCG(cx, &cg); + if (script && funpob) + script->flags |= JSSF_SAVED_CALLER_FUN; #ifdef JS_SCOPE_DEPTH_METER if (script) { diff --git a/js/src/jsparse.h b/js/src/jsparse.h index 90499db0bdda..fbe6350e42c2 100644 --- a/js/src/jsparse.h +++ b/js/src/jsparse.h @@ -456,7 +456,8 @@ extern JSScript * js_CompileScript(JSContext *cx, JSObject *scopeChain, JSStackFrame *callerFrame, JSPrincipals *principals, uint32 tcflags, const jschar *chars, size_t length, - FILE *file, const char *filename, uintN lineno); + FILE *file, const char *filename, uintN lineno, + JSString *source = NULL); extern JSBool js_CompileFunctionBody(JSContext *cx, JSFunction *fun, JSPrincipals *principals, diff --git a/js/src/jsscript.h b/js/src/jsscript.h index 93e4be48e3ef..504570d1dffb 100644 --- a/js/src/jsscript.h +++ b/js/src/jsscript.h @@ -130,6 +130,7 @@ struct JSScript { #define JSSF_NO_SCRIPT_RVAL 0x01 /* no need for result value of last expression statement */ +#define JSSF_SAVED_CALLER_FUN 0x02 /* object 0 is caller function */ static JS_INLINE uintN StackDepth(JSScript *script)