зеркало из https://github.com/mozilla/gecko-dev.git
Bug 454184 - Implement eval caching (r=mrbkap).
This commit is contained in:
Родитель
30a3eb4e26
Коммит
73225b4ef7
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 <stdio.h>
|
||||
|
||||
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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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. */
|
||||
|
|
122
js/src/jsobj.cpp
122
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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Загрузка…
Ссылка в новой задаче