mirror of
https://github.com/bellard/quickjs.git
synced 2024-12-12 09:44:33 +08:00
Implements profiling support by hooking into the function execution. qjs outputs chrome devtools-compatible profiling data
This commit is contained in:
parent
6e2e68fd08
commit
cae84d06d5
6
Makefile
6
Makefile
@ -53,6 +53,9 @@ PREFIX?=/usr/local
|
|||||||
|
|
||||||
# include the code for BigFloat/BigDecimal and math mode
|
# include the code for BigFloat/BigDecimal and math mode
|
||||||
CONFIG_BIGNUM=y
|
CONFIG_BIGNUM=y
|
||||||
|
# Include code for profiling individual function calls.
|
||||||
|
# qjs will produce a file compatible with chrome devtools and other tools.
|
||||||
|
# CONFIG_PROFILE_CALLS=y
|
||||||
|
|
||||||
OBJDIR=.obj
|
OBJDIR=.obj
|
||||||
|
|
||||||
@ -148,6 +151,9 @@ ifeq ($(shell $(CC) -o /dev/null compat/test-closefrom.c 2>/dev/null && echo 1),
|
|||||||
DEFINES+=-DHAVE_CLOSEFROM
|
DEFINES+=-DHAVE_CLOSEFROM
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
ifdef CONFIG_PROFILE_CALLS
|
||||||
|
DEFINES+=-DCONFIG_PROFILE_CALLS
|
||||||
|
endif
|
||||||
|
|
||||||
CFLAGS+=$(DEFINES)
|
CFLAGS+=$(DEFINES)
|
||||||
CFLAGS_DEBUG=$(CFLAGS) -O0
|
CFLAGS_DEBUG=$(CFLAGS) -O0
|
||||||
|
77
qjs.c
77
qjs.c
@ -289,6 +289,10 @@ void help(void)
|
|||||||
#endif
|
#endif
|
||||||
"-T --trace trace memory allocation\n"
|
"-T --trace trace memory allocation\n"
|
||||||
"-d --dump dump the memory usage stats\n"
|
"-d --dump dump the memory usage stats\n"
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
"-p --profile FILE dump the profiling stats to FILENAME in a format that can be open in chromium devtools\n"
|
||||||
|
" --profile-sampling n collect function calls every 1/n times. Defaults to 1 (i.e. every call)\n"
|
||||||
|
#endif
|
||||||
" --memory-limit n limit the memory usage to 'n' bytes\n"
|
" --memory-limit n limit the memory usage to 'n' bytes\n"
|
||||||
" --stack-size n limit the stack size to 'n' bytes\n"
|
" --stack-size n limit the stack size to 'n' bytes\n"
|
||||||
" --unhandled-rejection dump unhandled promise rejections\n"
|
" --unhandled-rejection dump unhandled promise rejections\n"
|
||||||
@ -296,6 +300,39 @@ void help(void)
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
void profile_function_start(JSContext *ctx, JSAtom func, JSAtom filename, void *opaque) {
|
||||||
|
const char* func_str = JS_AtomToCString(ctx, func);
|
||||||
|
const char *func_str2 = func_str[0] ? func_str : "<anonymous>";
|
||||||
|
FILE *logfile = (FILE *)opaque;
|
||||||
|
// Format documented here:
|
||||||
|
// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview?tab=t.0#heading=h.yr4qxyxotyw
|
||||||
|
fprintf(logfile, "{"
|
||||||
|
"\"name\": \"%s\","
|
||||||
|
"\"cat\": \"js\","
|
||||||
|
"\"ph\": \"B\","
|
||||||
|
"\"ts\": %ld,"
|
||||||
|
"\"pid\": 1,"
|
||||||
|
"\"tid\": 1"
|
||||||
|
"},\n", func_str2, clock());
|
||||||
|
JS_FreeCString(ctx, func_str);
|
||||||
|
}
|
||||||
|
void profile_function_end(JSContext *ctx, JSAtom func, JSAtom filename, void *opaque) {
|
||||||
|
const char* func_str = JS_AtomToCString(ctx, func);
|
||||||
|
const char *func_str2 = func_str[0] ? func_str : "<anonymous>";
|
||||||
|
FILE *logfile = (FILE *)opaque;
|
||||||
|
fprintf(logfile, "{"
|
||||||
|
"\"name\": \"%s\","
|
||||||
|
"\"cat\": \"js\","
|
||||||
|
"\"ph\": \"E\","
|
||||||
|
"\"ts\": %ld,"
|
||||||
|
"\"pid\": 1,"
|
||||||
|
"\"tid\": 1"
|
||||||
|
"},\n", func_str2, clock());
|
||||||
|
JS_FreeCString(ctx, func_str);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
JSRuntime *rt;
|
JSRuntime *rt;
|
||||||
@ -306,6 +343,10 @@ int main(int argc, char **argv)
|
|||||||
int interactive = 0;
|
int interactive = 0;
|
||||||
int dump_memory = 0;
|
int dump_memory = 0;
|
||||||
int trace_memory = 0;
|
int trace_memory = 0;
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
FILE *profile_file = NULL;
|
||||||
|
uint32_t profile_sampling = 1;
|
||||||
|
#endif
|
||||||
int empty_run = 0;
|
int empty_run = 0;
|
||||||
int module = -1;
|
int module = -1;
|
||||||
int load_std = 0;
|
int load_std = 0;
|
||||||
@ -399,6 +440,29 @@ int main(int argc, char **argv)
|
|||||||
trace_memory++;
|
trace_memory++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
if (opt == 'p' || !strcmp(longopt, "profile")) {
|
||||||
|
if (*arg) {
|
||||||
|
profile_file = fopen(arg, "w");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (optind < argc) {
|
||||||
|
profile_file = fopen(argv[optind++], "w");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fprintf(stderr, "qjs: missing filename for -p\n");
|
||||||
|
exit(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!strcmp(longopt, "profile-sampling")) {
|
||||||
|
if (optind >= argc) {
|
||||||
|
fprintf(stderr, "expecting profile sampling");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
profile_sampling = (uint32_t)strtod(argv[optind++], NULL);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (!strcmp(longopt, "std")) {
|
if (!strcmp(longopt, "std")) {
|
||||||
load_std = 1;
|
load_std = 1;
|
||||||
continue;
|
continue;
|
||||||
@ -461,6 +525,12 @@ int main(int argc, char **argv)
|
|||||||
fprintf(stderr, "qjs: cannot allocate JS runtime\n");
|
fprintf(stderr, "qjs: cannot allocate JS runtime\n");
|
||||||
exit(2);
|
exit(2);
|
||||||
}
|
}
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
if(profile_file) {
|
||||||
|
JS_EnableProfileCalls(rt, profile_function_start, profile_function_end, profile_sampling, profile_file);
|
||||||
|
fprintf(profile_file, "{\"traceEvents\": [");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (memory_limit != 0)
|
if (memory_limit != 0)
|
||||||
JS_SetMemoryLimit(rt, memory_limit);
|
JS_SetMemoryLimit(rt, memory_limit);
|
||||||
if (stack_size != 0)
|
if (stack_size != 0)
|
||||||
@ -530,6 +600,13 @@ int main(int argc, char **argv)
|
|||||||
js_std_free_handlers(rt);
|
js_std_free_handlers(rt);
|
||||||
JS_FreeContext(ctx);
|
JS_FreeContext(ctx);
|
||||||
JS_FreeRuntime(rt);
|
JS_FreeRuntime(rt);
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
if(profile_file) {
|
||||||
|
// Inject an instant event at the end just to make sure it's valid JSON.
|
||||||
|
fprintf(profile_file, R"({"name": "end", "ph": "I", "ts": %ld, "pid": 1, "tid": 1, "s": "g"}]})", clock());
|
||||||
|
fclose(profile_file);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (empty_run && dump_memory) {
|
if (empty_run && dump_memory) {
|
||||||
clock_t t[5];
|
clock_t t[5];
|
||||||
|
129
quickjs.c
129
quickjs.c
@ -302,6 +302,13 @@ struct JSRuntime {
|
|||||||
JSNumericOperations bigfloat_ops;
|
JSNumericOperations bigfloat_ops;
|
||||||
JSNumericOperations bigdecimal_ops;
|
JSNumericOperations bigdecimal_ops;
|
||||||
uint32_t operator_count;
|
uint32_t operator_count;
|
||||||
|
#endif
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
ProfileEventHandler *profile_function_start;
|
||||||
|
ProfileEventHandler *profile_function_end;
|
||||||
|
void *profile_opaque;
|
||||||
|
uint32_t profile_sampling;
|
||||||
|
uint32_t profile_sample_count;
|
||||||
#endif
|
#endif
|
||||||
void *user_opaque;
|
void *user_opaque;
|
||||||
};
|
};
|
||||||
@ -621,6 +628,10 @@ typedef struct JSFunctionBytecode {
|
|||||||
int pc2line_len;
|
int pc2line_len;
|
||||||
uint8_t *pc2line_buf;
|
uint8_t *pc2line_buf;
|
||||||
char *source;
|
char *source;
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
/* Class.function or Object.function or just function */
|
||||||
|
JSAtom full_func_name_cache;
|
||||||
|
#endif
|
||||||
} debug;
|
} debug;
|
||||||
} JSFunctionBytecode;
|
} JSFunctionBytecode;
|
||||||
|
|
||||||
@ -5546,8 +5557,17 @@ void __JS_FreeValueRT(JSRuntime *rt, JSValue v)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case JS_TAG_OBJECT:
|
|
||||||
case JS_TAG_FUNCTION_BYTECODE:
|
case JS_TAG_FUNCTION_BYTECODE:
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
{
|
||||||
|
JSFunctionBytecode *b = JS_VALUE_GET_PTR(v);
|
||||||
|
// In STRICT js_mode, there is no "debug".
|
||||||
|
if (!(b->js_mode & JS_MODE_STRICT) && b->debug.full_func_name_cache != JS_ATOM_NULL) {
|
||||||
|
JS_FreeAtomRT(rt, b->debug.full_func_name_cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
case JS_TAG_OBJECT:
|
||||||
{
|
{
|
||||||
JSGCObjectHeader *p = JS_VALUE_GET_PTR(v);
|
JSGCObjectHeader *p = JS_VALUE_GET_PTR(v);
|
||||||
if (rt->gc_phase != JS_GC_PHASE_REMOVE_CYCLES) {
|
if (rt->gc_phase != JS_GC_PHASE_REMOVE_CYCLES) {
|
||||||
@ -5881,6 +5901,20 @@ BOOL JS_IsLiveObject(JSRuntime *rt, JSValueConst obj)
|
|||||||
return !p->free_mark;
|
return !p->free_mark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void JS_EnableProfileCalls(JSRuntime *rt, ProfileEventHandler *on_start, ProfileEventHandler *on_end, uint32_t sampling, void *opaque_data)
|
||||||
|
{
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
rt->profile_function_start = on_start;
|
||||||
|
rt->profile_function_end = on_end;
|
||||||
|
rt->profile_opaque = opaque_data;
|
||||||
|
// If sampling == 0, it's interpreted as "no sampling" which means we log 1/1 calls.
|
||||||
|
rt->profile_sampling = sampling > 0 ? sampling : 1;
|
||||||
|
rt->profile_sample_count = 0;
|
||||||
|
#else
|
||||||
|
fprintf(stderr, "QuickJS was not compiled with -DCONFIG_PROFILE_CALLS. Profiling is disabled.");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/* Compute memory used by various object types */
|
/* Compute memory used by various object types */
|
||||||
/* XXX: poor man's approach to handling multiply referenced objects */
|
/* XXX: poor man's approach to handling multiply referenced objects */
|
||||||
typedef struct JSMemoryUsage_helper {
|
typedef struct JSMemoryUsage_helper {
|
||||||
@ -6529,6 +6563,64 @@ static const char *get_func_name(JSContext *ctx, JSValueConst func)
|
|||||||
return JS_ToCString(ctx, val);
|
return JS_ToCString(ctx, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Computes the full name of the function including the constructor of the
|
||||||
|
object it's being executed on, if any. For example, this may output
|
||||||
|
"MyClass.my_function" or if there is no "this", just "my_function". */
|
||||||
|
static JSAtom get_full_func_name(JSContext *ctx, JSValueConst func, JSValueConst this_obj)
|
||||||
|
{
|
||||||
|
JSAtom result_atom;
|
||||||
|
const char *func_str = get_func_name(ctx, func);
|
||||||
|
if (!func_str || func_str[0] == '\0') {
|
||||||
|
JS_FreeCString(ctx, func_str);
|
||||||
|
return JS_ATOM_NULL;
|
||||||
|
}
|
||||||
|
// If "this" isn't an object, return just the name.
|
||||||
|
if (JS_VALUE_GET_TAG(this_obj) != JS_TAG_OBJECT) {
|
||||||
|
result_atom = JS_NewAtom(ctx, func_str);
|
||||||
|
JS_FreeCString(ctx, func_str);
|
||||||
|
return result_atom;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSValue ctor = JS_GetProperty(ctx, this_obj, JS_ATOM_constructor);
|
||||||
|
const char *ctor_str = get_func_name(ctx, ctor);
|
||||||
|
size_t func_len = strlen(func_str);
|
||||||
|
JSValue result_val;
|
||||||
|
|
||||||
|
if (ctor_str == NULL || ctor_str[0] == '\0') {
|
||||||
|
// Invalid constructor, use <unknown>
|
||||||
|
StringBuffer sb;
|
||||||
|
char prefix[] = "<unknown>.";
|
||||||
|
if (string_buffer_init2(ctx, &sb, sizeof(prefix) + func_len, 0)) {
|
||||||
|
JS_FreeCString(ctx, func_str);
|
||||||
|
JS_FreeCString(ctx, ctor_str);
|
||||||
|
JS_FreeValue(ctx, ctor);
|
||||||
|
return JS_ATOM_NULL;
|
||||||
|
}
|
||||||
|
string_buffer_write8(&sb, (const uint8_t *)prefix, sizeof(prefix));
|
||||||
|
string_buffer_write8(&sb, (const uint8_t *)func_str, func_len);
|
||||||
|
result_val = string_buffer_end(&sb);
|
||||||
|
} else {
|
||||||
|
StringBuffer sb;
|
||||||
|
size_t ctor_len = strlen(ctor_str);
|
||||||
|
if (string_buffer_init2(ctx, &sb, ctor_len + 1 + func_len, 0)) {
|
||||||
|
JS_FreeCString(ctx, func_str);
|
||||||
|
JS_FreeCString(ctx, ctor_str);
|
||||||
|
JS_FreeValue(ctx, ctor);
|
||||||
|
return JS_ATOM_NULL;
|
||||||
|
}
|
||||||
|
string_buffer_write8(&sb, (const uint8_t *)ctor_str, ctor_len);
|
||||||
|
string_buffer_write8(&sb, (const uint8_t *)".", 1);
|
||||||
|
string_buffer_write8(&sb, (const uint8_t *)func_str, func_len);
|
||||||
|
result_val = string_buffer_end(&sb);
|
||||||
|
}
|
||||||
|
result_atom = JS_ValueToAtom(ctx, result_val);
|
||||||
|
JS_FreeValue(ctx, result_val);
|
||||||
|
JS_FreeCString(ctx, func_str);
|
||||||
|
JS_FreeCString(ctx, ctor_str);
|
||||||
|
JS_FreeValue(ctx, ctor);
|
||||||
|
return result_atom;
|
||||||
|
}
|
||||||
|
|
||||||
#define JS_BACKTRACE_FLAG_SKIP_FIRST_LEVEL (1 << 0)
|
#define JS_BACKTRACE_FLAG_SKIP_FIRST_LEVEL (1 << 0)
|
||||||
/* only taken into account if filename is provided */
|
/* only taken into account if filename is provided */
|
||||||
#define JS_BACKTRACE_FLAG_SINGLE_LEVEL (1 << 1)
|
#define JS_BACKTRACE_FLAG_SINGLE_LEVEL (1 << 1)
|
||||||
@ -16169,6 +16261,10 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
|
|||||||
JSValue *local_buf, *stack_buf, *var_buf, *arg_buf, *sp, ret_val, *pval;
|
JSValue *local_buf, *stack_buf, *var_buf, *arg_buf, *sp, ret_val, *pval;
|
||||||
JSVarRef **var_refs;
|
JSVarRef **var_refs;
|
||||||
size_t alloca_size;
|
size_t alloca_size;
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
JSAtom full_func_name = JS_ATOM_NULL;
|
||||||
|
const int must_sample = rt->profile_sampling && rt->profile_sample_count == 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if !DIRECT_DISPATCH
|
#if !DIRECT_DISPATCH
|
||||||
#define SWITCH(pc) switch (opcode = *pc++)
|
#define SWITCH(pc) switch (opcode = *pc++)
|
||||||
@ -16232,7 +16328,22 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
|
|||||||
(JSValueConst *)argv, flags);
|
(JSValueConst *)argv, flags);
|
||||||
}
|
}
|
||||||
b = p->u.func.function_bytecode;
|
b = p->u.func.function_bytecode;
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
if (unlikely(must_sample)) {
|
||||||
|
if (!(b->js_mode & JS_MODE_STRICT)) {
|
||||||
|
if (!b->debug.full_func_name_cache) {
|
||||||
|
b->debug.full_func_name_cache = get_full_func_name(caller_ctx, func_obj, this_obj);
|
||||||
|
}
|
||||||
|
full_func_name = b->debug.full_func_name_cache;
|
||||||
|
} else {
|
||||||
|
// Even if we can't cache it, we need to compute it to report the function execution.
|
||||||
|
full_func_name = get_full_func_name(caller_ctx, func_obj, this_obj);
|
||||||
|
}
|
||||||
|
if (likely(rt->profile_function_start)) {
|
||||||
|
rt->profile_function_start(caller_ctx, full_func_name, b->debug.filename, rt->profile_opaque);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (unlikely(argc < b->arg_count || (flags & JS_CALL_FLAG_COPY_ARGV))) {
|
if (unlikely(argc < b->arg_count || (flags & JS_CALL_FLAG_COPY_ARGV))) {
|
||||||
arg_allocated_size = b->arg_count;
|
arg_allocated_size = b->arg_count;
|
||||||
} else {
|
} else {
|
||||||
@ -18701,6 +18812,20 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
rt->current_stack_frame = sf->prev_frame;
|
rt->current_stack_frame = sf->prev_frame;
|
||||||
|
#ifdef CONFIG_PROFILE_CALLS
|
||||||
|
if (unlikely(must_sample)) {
|
||||||
|
if (likely(rt->profile_function_end)) {
|
||||||
|
rt->profile_function_end(caller_ctx, full_func_name, b->debug.filename, rt->profile_opaque);
|
||||||
|
}
|
||||||
|
if (b->js_mode & JS_MODE_STRICT) {
|
||||||
|
// If we weren't able to cache it, we have to free it right away (and sadly recreate it later).
|
||||||
|
JS_FreeAtom(caller_ctx, full_func_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unlikely(rt->profile_sampling)) {
|
||||||
|
rt->profile_sample_count = (rt->profile_sample_count + 1) % rt->profile_sampling;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return ret_val;
|
return ret_val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
25
quickjs.h
25
quickjs.h
@ -350,6 +350,31 @@ void JS_MarkValue(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func);
|
|||||||
void JS_RunGC(JSRuntime *rt);
|
void JS_RunGC(JSRuntime *rt);
|
||||||
JS_BOOL JS_IsLiveObject(JSRuntime *rt, JSValueConst obj);
|
JS_BOOL JS_IsLiveObject(JSRuntime *rt, JSValueConst obj);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function type for handling JavaScript profiling events.
|
||||||
|
*
|
||||||
|
* @param func Function name as a JSAtom. May be in the format "Constructor.name"
|
||||||
|
* when the function is executed in a constructor's context (i.e.,
|
||||||
|
* with 'this' binding)
|
||||||
|
* @param filename Name of the source file containing the function, as a JSAtom
|
||||||
|
* @param opaque_data User data that was originally passed to JS_EnableProfileCalls.
|
||||||
|
* Same value is provided to both start and end handlers
|
||||||
|
*/
|
||||||
|
typedef void ProfileEventHandler(JSContext *ctx, JSAtom func, JSAtom filename, void *opaque_data);
|
||||||
|
/**
|
||||||
|
* Enables function call profiling for the JavaScript runtime.
|
||||||
|
*
|
||||||
|
* NOTE: This function only works if QuickJS was compiled with -DCONFIG_PROFILE_CALLS flag.
|
||||||
|
*
|
||||||
|
* @param on_start Callback called when a function starts.
|
||||||
|
* @param on_end Callback called when a function ends.
|
||||||
|
* @param sampling Controls profiling frequency: only 1/sampling function calls are
|
||||||
|
* instrumented. Must be ≥ 1. Example: if sampling=4, only 25% of
|
||||||
|
* function calls will trigger the handlers.
|
||||||
|
* @param opaque_data Optional user data passed to both handlers. Can be NULL.
|
||||||
|
*/
|
||||||
|
void JS_EnableProfileCalls(JSRuntime *rt, ProfileEventHandler *on_start, ProfileEventHandler *on_end, uint32_t sampling, void *opaque_data);
|
||||||
|
|
||||||
JSContext *JS_NewContext(JSRuntime *rt);
|
JSContext *JS_NewContext(JSRuntime *rt);
|
||||||
void JS_FreeContext(JSContext *s);
|
void JS_FreeContext(JSContext *s);
|
||||||
JSContext *JS_DupContext(JSContext *ctx);
|
JSContext *JS_DupContext(JSContext *ctx);
|
||||||
|
Loading…
Reference in New Issue
Block a user