pebble/tests/fw/javascript/test_rocky_api_memory.c
2025-01-27 11:38:16 -08:00

265 lines
8.1 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_memory.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "syscall/syscall.h"
#include <jmem/jmem-heap.h>
// Standard
#include <string.h>
// Fakes
#include "fake_app_timer.h"
#include "fake_logging.h"
#include "fake_pbl_malloc.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
size_t heap_bytes_free(void) {
return 123456;
}
static int s_sys_analytics_inc_call_count;
void sys_analytics_inc(AnalyticsMetric metric, AnalyticsClient client) {
cl_assert_equal_i(metric, ANALYTICS_APP_METRIC_MEM_ROCKY_RECURSIVE_MEMORYPRESSURE_EVENT_COUNT);
++s_sys_analytics_inc_call_count;
}
static const RockyGlobalAPI *s_api[] = {
&MEMORY_APIS,
NULL,
};
static bool s_skip_pbl_malloc_check;
#define assert_oom_app_fault() \
cl_assert_equal_i(s_app_heap_analytics_log_rocky_heap_oom_fault_call_count, 1)
#define assert_no_oom_app_fault() \
cl_assert_equal_i(s_app_heap_analytics_log_rocky_heap_oom_fault_call_count, 0)
void test_rocky_api_memory__initialize(void) {
s_sys_analytics_inc_call_count = 0;
s_app_heap_analytics_log_rocky_heap_oom_fault_call_count = 0;
fake_pbl_malloc_clear_tracking();
s_skip_pbl_malloc_check = false;
s_log_internal__expected = NULL;
rocky_runtime_context_init();
jerry_init(JERRY_INIT_EMPTY);
rocky_global_init(s_api);
}
void test_rocky_api_memory__cleanup(void) {
rocky_global_deinit();
jerry_cleanup();
rocky_runtime_context_deinit();
if (!s_skip_pbl_malloc_check) {
fake_pbl_malloc_check_net_allocs();
}
}
void test_rocky_api_memory__event(void) {
cl_assert_equal_b(false, rocky_global_has_event_handlers("memorypressure"));
jmem_heap_stats_t before_stats = {};
jmem_heap_get_stats(&before_stats);
EXECUTE_SCRIPT("_rocky.on('memorypressure', function(){});");
// After registering a handler for 'memorypressure', expect a drop of more than N bytes of heap
// because of the reservation of headroom space:
jmem_heap_stats_t after_stats = {};
jmem_heap_get_stats(&after_stats);
cl_assert(after_stats.allocated_bytes - before_stats.allocated_bytes >=
ROCKY_API_MEMORY_HEADROOM_DESIRED_SIZE_BYTES);
cl_assert_equal_b(true, rocky_global_has_event_handlers("memorypressure"));
}
void test_rocky_api_memory__oom_app_fault_if_handler_allocates_more_than_headroom(void) {
s_skip_pbl_malloc_check = true;
cl_assert_passert(EXECUTE_SCRIPT(
"var data = [];\n"
"_rocky.on('memorypressure', function(){\n"
" var handlerData = [];\n"
" for (var i = 0; i < 100000; i++) {handlerData.push(i);}\n"
"});\n"
"for (var i = 0; i < 100000; i++) {data.push(i);}\n"
));
assert_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 1);
}
void test_rocky_api_memory__no_oom_app_fault_if_handler_frees_up_enough_memory_empty_array(void) {
// Note to the reader: the lifecycle of `data` is not what you might think it is on first sight:
// When `data = [];` executes, the original `data` will still be retained, because the original
// execution context is still on the stack. Only after the the 'memorypressure' handler returns
// and that for(){} block finishes, is the original `data` released!
EXECUTE_SCRIPT(
"var data = [];\n"
"var level = undefined;"
"_rocky.on('memorypressure', function(e){\n"
" level = e.level;\n"
" data = [];\n"
"});\n"
"for (var i = 0; i < 100000; i++) {data.push(i);}\n"
);
assert_no_oom_app_fault();
ASSERT_JS_GLOBAL_EQUALS_S("level", "high");
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
void test_rocky_api_memory__no_oom_app_fault_if_handler_frees_up_enough_memory_empty_object(void) {
EXECUTE_SCRIPT(
"var data = {};\n"
"_rocky.on('memorypressure', function(e){\n"
" data = {};\n"
"});\n"
"for (var i = 0; i < 100000; i++) {data[i] = i;}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
void test_rocky_api_memory__no_oom_app_fault_if_handler_frees_up_enough_memory_put_props_for(void) {
// This example uses a lot of properties on an Object to store things.
// When running out of memory, these are dropped to free up memory, using the `delete` operator.
EXECUTE_SCRIPT(
"var first = 0;\n"
"var i = 0;\n"
"var obj = {};\n"
"_rocky.on('memorypressure', function(e){\n"
" for (var j = first; j < i; j++) {\n"
" delete obj[j];\n"
" }\n"
" first = i;\n"
"});\n"
"for (i = first; i < 100000; i++) {\n"
" obj[i] = i;"
"}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
#if 0
// This doesn't work at the moment, because the `in` operator allocates a ton of memory... :( for
// the same reason as why Array.pop() has a footprint that's proportional to the number of elements.
void test_rocky_api_memory
__no_oom_app_fault_if_handler_frees_up_enough_mem_put_props_for_in(void) {
EXECUTE_SCRIPT(
"var obj = {};\n"
"_rocky.on('memorypressure', function(e){\n"
" for (var p in obj) {\n"
" delete obj[p];\n"
" }\n"
"});\n"
"for (var i = 0; i < 100000; i++) {\n"
" obj['' + i] = i;"
"}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
#endif
#if 0
// This doesn't work because the putting the `length` property of an array end up calling
// ecma_op_object_get_property_names, which is has a memory footprint proportional to the number of
// elements/properties..
void test_rocky_api_memory
__no_oom_app_fault_if_handler_frees_up_enough_memory_put_length(void) {
EXECUTE_SCRIPT(
"var cache = [];\n"
"_rocky.on('memorypressure', function(event) {\n"
" while (cache.length > 0) {\n"
" delete cache[cache.length - 1];\n"
" --cache.length;\n"
" }\n"
"})\n;"
"for (var i = 0; i < 100000; i++) {\n"
" cache.push(i);\n"
"}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
#endif
#if 0
// This doesn't work at the moment, because https://github.com/Samsung/jerryscript/issues/1370
void test_rocky_api_memory
__no_oom_app_fault_if_handler_frees_up_enough_memory_simple(void) {
EXECUTE_SCRIPT(
"var cache = [];\n"
"_rocky.on('memorypressure', function(event) {\n"
" while (cache.length > 0) {\n"
" cache.pop();\n"
" }\n"
"})\n;"
"for (var i = 0; i < 100000; i++) {\n"
" cache.push(i);\n"
"}\n"
);
assert_no_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
}
#endif
void test_rocky_api_memory__oom_app_fault_if_handler_does_not_free_up_enough_memory(void) {
s_skip_pbl_malloc_check = true;
s_log_internal__expected_regex = (const char *[]){
"Memory pressure level: high",
"heap size: [0-9]+, alloc'd: [0-9]+, waste: [0-9]+, largest free block: [0-9]+,",
"used blocks: [0-9]+, free blocks: [0-9]+",
"Fatal Error: 10",
NULL };
cl_assert_passert(EXECUTE_SCRIPT(
"var data = [];\n"
"var shouldContinue = true;\n"
"_rocky.on('memorypressure', function(){\n"
" shouldContinue = false;\n"
"});\n"
"for (var i = 0; shouldContinue && i < 100000; i++) {data.push(i);}\n"
));
assert_oom_app_fault();
cl_assert_equal_i(s_sys_analytics_inc_call_count, 0);
cl_assert(*s_log_internal__expected_regex == NULL);
}