-
Notifications
You must be signed in to change notification settings - Fork 200
Add per-context memory accounting and limits #1145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -481,6 +481,8 @@ struct JSContext { | |
const char *input, size_t input_len, | ||
const char *filename, int line, int flags, int scope_idx); | ||
void *user_opaque; | ||
size_t mem_limit; /* 0 = unlimited */ | ||
size_t mem_used; /* tracked allocations */ | ||
}; | ||
|
||
typedef union JSFloat64Union { | ||
|
@@ -1552,72 +1554,184 @@ void *js_mallocz_rt(JSRuntime *rt, size_t size) | |
/* Throw out of memory in case of error */ | ||
void *js_calloc(JSContext *ctx, size_t count, size_t size) | ||
{ | ||
void *ptr; | ||
ptr = js_calloc_rt(ctx->rt, count, size); | ||
size_t req_size; | ||
if (unlikely(__builtin_mul_overflow(count, size, &req_size))) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; // overflow | ||
} | ||
|
||
// upfront check against budget | ||
if (unlikely(ctx->mem_limit && req_size > (ctx->mem_limit - ctx->mem_used))) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
void *ptr = js_calloc_rt(ctx->rt, count, size); | ||
if (unlikely(!ptr)) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
size_t actual = js_malloc_usable_size_rt(ctx->rt, ptr); | ||
|
||
if (unlikely(ctx->mem_limit && ctx->mem_used + actual > ctx->mem_limit)) { | ||
js_free_rt(ctx->rt, ptr); | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
Comment on lines
+1575
to
+1581
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is arguably excessive. js_malloc_usable_size_rt (wrapper around malloc_usable_size on linux) is not necessarily very cheap. Probably not worth it to avoid going a few bytes over the limit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, I should only check once before the pre-allocation. Fragmentation and the actual physical size shouldn't be included in the limits. I will make the change |
||
|
||
ctx->mem_used += actual; | ||
return ptr; | ||
} | ||
|
||
|
||
/* Throw out of memory in case of error */ | ||
void *js_malloc(JSContext *ctx, size_t size) | ||
{ | ||
void *ptr; | ||
ptr = js_malloc_rt(ctx->rt, size); | ||
if (unlikely(ctx->mem_limit && size > (ctx->mem_limit - ctx->mem_used))) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
void *ptr = js_malloc_rt(ctx->rt, size); | ||
if (unlikely(!ptr)) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
size_t actual = js_malloc_usable_size_rt(ctx->rt, ptr); | ||
|
||
if (unlikely(ctx->mem_limit && ctx->mem_used + actual > ctx->mem_limit)) { | ||
js_free_rt(ctx->rt, ptr); | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
ctx->mem_used += actual; | ||
return ptr; | ||
} | ||
|
||
|
||
/* Throw out of memory in case of error */ | ||
void *js_mallocz(JSContext *ctx, size_t size) | ||
{ | ||
void *ptr; | ||
ptr = js_mallocz_rt(ctx->rt, size); | ||
// upfront check | ||
if (unlikely(ctx->mem_limit && size > (ctx->mem_limit - ctx->mem_used))) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
void *ptr = js_mallocz_rt(ctx->rt, size); | ||
if (unlikely(!ptr)) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
size_t actual = js_malloc_usable_size_rt(ctx->rt, ptr); | ||
|
||
if (unlikely(ctx->mem_limit && ctx->mem_used + actual > ctx->mem_limit)) { | ||
js_free_rt(ctx->rt, ptr); | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
ctx->mem_used += actual; | ||
return ptr; | ||
} | ||
|
||
|
||
void js_free(JSContext *ctx, void *ptr) | ||
{ | ||
if (unlikely(!ptr)) | ||
return; | ||
size_t actual = js_malloc_usable_size(ctx, ptr); | ||
ctx->mem_used -= actual; | ||
js_free_rt(ctx->rt, ptr); | ||
} | ||
|
||
/* Throw out of memory in case of error */ | ||
void *js_realloc(JSContext *ctx, void *ptr, size_t size) | ||
{ | ||
void *ret; | ||
ret = js_realloc_rt(ctx->rt, ptr, size); | ||
size_t old_size = 0; | ||
if (ptr) | ||
old_size = js_malloc_usable_size_rt(ctx->rt, ptr); | ||
|
||
if (unlikely(ctx->mem_limit && size > 0 && | ||
size > (ctx->mem_limit - (ctx->mem_used - old_size)))) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
void *ret = js_realloc_rt(ctx->rt, ptr, size); | ||
if (unlikely(!ret && size != 0)) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
if (likely(ret)) { | ||
size_t new_size = js_malloc_usable_size_rt(ctx->rt, ret); | ||
ctx->mem_used += new_size - old_size; | ||
|
||
if (unlikely(ctx->mem_limit && ctx->mem_used > ctx->mem_limit)) { | ||
js_free_rt(ctx->rt, ret); | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
} else { | ||
ctx->mem_used -= old_size; // realloc(...,0) frees memory | ||
} | ||
|
||
return ret; | ||
} | ||
|
||
|
||
|
||
/* store extra allocated size in *pslack if successful */ | ||
void *js_realloc2(JSContext *ctx, void *ptr, size_t size, size_t *pslack) | ||
{ | ||
void *ret; | ||
ret = js_realloc_rt(ctx->rt, ptr, size); | ||
size_t old_size = 0; | ||
if (ptr) | ||
old_size = js_malloc_usable_size_rt(ctx->rt, ptr); | ||
|
||
// upfront check | ||
if (unlikely(ctx->mem_limit && size > 0)) { | ||
size_t remaining = ctx->mem_limit - (ctx->mem_used - old_size); | ||
if (unlikely(size > remaining)) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
} | ||
|
||
void *ret = js_realloc_rt(ctx->rt, ptr, size); | ||
if (unlikely(!ret && size != 0)) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
if (pslack) { | ||
|
||
if (likely(ret)) { | ||
size_t new_size = js_malloc_usable_size_rt(ctx->rt, ret); | ||
*pslack = (new_size > size) ? new_size - size : 0; | ||
ctx->mem_used += new_size - old_size; | ||
|
||
if (unlikely(ctx->mem_limit && ctx->mem_used > ctx->mem_limit)) { | ||
js_free_rt(ctx->rt, ret); | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
if (pslack) | ||
*pslack = (new_size > size) ? new_size - size : 0; | ||
} else { | ||
// realloc with size=0 frees memory | ||
ctx->mem_used -= old_size; | ||
if (pslack) | ||
*pslack = 0; | ||
} | ||
|
||
return ret; | ||
} | ||
|
||
|
||
|
||
size_t js_malloc_usable_size(JSContext *ctx, const void *ptr) | ||
{ | ||
return js_malloc_usable_size_rt(ctx->rt, ptr); | ||
|
@@ -2056,11 +2170,31 @@ static JSString *js_alloc_string_rt(JSRuntime *rt, int max_len, int is_wide_char | |
static JSString *js_alloc_string(JSContext *ctx, int max_len, int is_wide_char) | ||
{ | ||
JSString *p; | ||
|
||
// upfront budget check: string object header + chars | ||
size_t approx_size = sizeof(JSString) + | ||
(size_t)max_len * (is_wide_char ? 2 : 1); | ||
|
||
if (unlikely(ctx->mem_limit && approx_size > (ctx->mem_limit - ctx->mem_used))) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
Comment on lines
+2174
to
+2181
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is better handled by breaking out js_alloc_string_rt into two separate functions, one that calls js_malloc_rt and then calls the second function to initialize the string. Call said second function from here with memory allocated with js_malloc. |
||
|
||
p = js_alloc_string_rt(ctx->rt, max_len, is_wide_char); | ||
if (unlikely(!p)) { | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
size_t actual = js_malloc_usable_size_rt(ctx->rt, p); | ||
|
||
if (unlikely(ctx->mem_limit && ctx->mem_used + actual > ctx->mem_limit)) { | ||
js_free_rt(ctx->rt, p); | ||
JS_ThrowOutOfMemory(ctx); | ||
return NULL; | ||
} | ||
|
||
ctx->mem_used += actual; | ||
return p; | ||
} | ||
|
||
|
@@ -2311,6 +2445,8 @@ JSContext *JS_NewContextRaw(JSRuntime *rt) | |
ctx->error_back_trace = JS_UNDEFINED; | ||
ctx->error_prepare_stack = JS_UNDEFINED; | ||
ctx->error_stack_trace_limit = js_int32(10); | ||
ctx->mem_limit = 0; | ||
ctx->mem_used = 0; | ||
init_list_head(&ctx->loaded_modules); | ||
|
||
JS_AddIntrinsicBasicObjects(ctx); | ||
|
@@ -2379,6 +2515,21 @@ JSValue JS_GetFunctionProto(JSContext *ctx) | |
return js_dup(ctx->function_proto); | ||
} | ||
|
||
void JS_SetContextMemoryLimit(JSContext *ctx, size_t limit) | ||
{ | ||
ctx->mem_limit = limit; | ||
} | ||
|
||
void JS_ResetContextMemory(JSContext *ctx) | ||
{ | ||
ctx->mem_used = 0; | ||
} | ||
|
||
size_t JS_GetContextMemoryUsage(JSContext *ctx) | ||
{ | ||
return ctx->mem_used; | ||
} | ||
|
||
typedef enum JSFreeModuleEnum { | ||
JS_FREE_MODULE_ALL, | ||
JS_FREE_MODULE_NOT_RESOLVED, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't use __builtin_mul_overflow, not universally supported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're right, I only put it there as a draft.
Instead of using __builtin_mul_overflow, we can #include <stdckdint.h>.
For portability, I'll add a compile-time fallback with a pure C implementation as backup.