]> git.saurik.com Git - apple/xnu.git/blobdiff - tests/memorystatus_freeze_test.c
xnu-7195.101.1.tar.gz
[apple/xnu.git] / tests / memorystatus_freeze_test.c
index 471312f805abb443a6ccb51be05e63d7a97258fa..eeffaf9edbda1b34b9dfff8d6625da3ca38d8448 100644 (file)
@@ -1,7 +1,9 @@
 #include <stdio.h>
 #include <signal.h>
+#include <sys/proc.h>
 #include <sys/sysctl.h>
 #include <sys/kern_memorystatus.h>
+#include <sys/kern_memorystatus_freeze.h>
 #include <time.h>
 #include <mach-o/dyld.h>
 #include <mach/mach_vm.h>
@@ -38,7 +40,10 @@ T_GLOBAL_META(
        X(IS_FREEZABLE_NOT_AS_EXPECTED) \
        X(MEMSTAT_PRIORITY_CHANGE_FAILED) \
        X(INVALID_ALLOCATE_PAGES_ARGUMENTS) \
-       X(EXIT_CODE_MAX)
+       X(FROZEN_BIT_SET) \
+       X(FROZEN_BIT_NOT_SET) \
+       X(MEMORYSTATUS_CONTROL_ERROR) \
+       X(EXIT_CODE_MAX) \
 
 #define EXIT_CODES_ENUM(VAR) VAR,
 enum exit_codes_num {
@@ -64,7 +69,7 @@ get_vmpage_size()
 static pid_t child_pid = -1;
 static int freeze_count = 0;
 
-void move_to_idle_band(void);
+void move_to_idle_band(pid_t);
 void run_freezer_test(int);
 void freeze_helper_process(void);
 /* Gets and optionally sets the freeze pages max threshold */
@@ -245,7 +250,7 @@ get_rprvt(pid_t pid)
 }
 
 void
-move_to_idle_band(void)
+move_to_idle_band(pid_t pid)
 {
        memorystatus_priority_properties_t props;
        /*
@@ -259,7 +264,7 @@ move_to_idle_band(void)
         * This requires us to run as root (in the absence of entitlement).
         * Hence the T_META_ASROOT(true) in the T_HELPER_DECL.
         */
-       if (memorystatus_control(MEMORYSTATUS_CMD_SET_PRIORITY_PROPERTIES, getpid(), 0, &props, sizeof(props))) {
+       if (memorystatus_control(MEMORYSTATUS_CMD_SET_PRIORITY_PROPERTIES, pid, 0, &props, sizeof(props))) {
                exit(MEMSTAT_PRIORITY_CHANGE_FAILED);
        }
 }
@@ -312,24 +317,31 @@ freeze_helper_process(void)
        T_QUIET; T_ASSERT_POSIX_SUCCESS(kill(child_pid, SIGUSR1), "failed to send SIGUSR1 to child process");
 }
 
+static void
+skip_if_freezer_is_disabled()
+{
+       int freeze_enabled;
+       size_t length = sizeof(freeze_enabled);
+
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(sysctlbyname("vm.freeze_enabled", &freeze_enabled, &length, NULL, 0),
+           "failed to query vm.freeze_enabled");
+       if (!freeze_enabled) {
+               /* If freezer is disabled, skip the test. This can happen due to disk space shortage. */
+               T_SKIP("Freeze has been disabled. Skipping test.");
+       }
+}
+
 void
 run_freezer_test(int num_pages)
 {
-       int ret, freeze_enabled;
+       int ret;
        char sz_str[50];
        char **launch_tool_args;
        char testpath[PATH_MAX];
        uint32_t testpath_buf_size;
        dispatch_source_t ds_freeze, ds_proc;
-       size_t length;
 
-       length = sizeof(freeze_enabled);
-       T_QUIET; T_ASSERT_POSIX_SUCCESS(sysctlbyname("vm.freeze_enabled", &freeze_enabled, &length, NULL, 0),
-           "failed to query vm.freeze_enabled");
-       if (!freeze_enabled) {
-               /* If freezer is disabled, skip the test. This can happen due to disk space shortage. */
-               T_SKIP("Freeze has been disabled. Skipping test.");
-       }
+       skip_if_freezer_is_disabled();
 
        signal(SIGUSR1, SIG_IGN);
        ds_freeze = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGUSR1, 0, dispatch_get_main_queue());
@@ -467,7 +479,7 @@ allocate_pages(int num_pages)
                }
        });
        dispatch_activate(ds_signal);
-       move_to_idle_band();
+       move_to_idle_band(getpid());
 
        dispatch_main();
 }
@@ -537,11 +549,12 @@ T_HELPER_DECL(frozen_background, "Frozen background process", T_META_ASROOT(true
 
 /* Launches the frozen_background helper as a managed process. */
 static pid_t
-launch_frozen_background_process()
+launch_background_helper(const char* variant)
 {
        pid_t pid;
        char **launch_tool_args;
        char testpath[PATH_MAX];
+       char *variant_cpy = strdup(variant);
        uint32_t testpath_buf_size;
        int ret;
 
@@ -551,7 +564,7 @@ launch_frozen_background_process()
        launch_tool_args = (char *[]){
                testpath,
                "-n",
-               "frozen_background",
+               variant_cpy,
                NULL
        };
        ret = dt_launch_tool(&pid, launch_tool_args, false, NULL, NULL);
@@ -562,6 +575,7 @@ launch_frozen_background_process()
        /* Set the process's managed bit, so that the kernel treats this process like an app instead of a sysproc. */
        ret = memorystatus_control(MEMORYSTATUS_CMD_SET_PROCESS_IS_MANAGED, pid, 1, NULL, 0);
        T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "memorystatus_control");
+       free(variant_cpy);
        return pid;
 }
 
@@ -590,9 +604,6 @@ freeze_process(pid_t pid)
 static void
 memorystatus_assertion_test_demote_frozen()
 {
-#if !CONFIG_EMBEDDED
-       T_SKIP("Freezing processes is only supported on embedded");
-#endif
        /*
         * Test that if we assert a priority on a process, freeze it, and then demote all frozen processes, it does not get demoted below the asserted priority.
         * Then remove thee assertion, and ensure it gets demoted properly.
@@ -600,7 +611,7 @@ memorystatus_assertion_test_demote_frozen()
        /* these values will remain fixed during testing */
        int             active_limit_mb = 15;   /* arbitrary */
        int             inactive_limit_mb = 7;  /* arbitrary */
-       int             demote_value = 1;
+       __block int             demote_value = 1;
        /* Launch the child process, and elevate its priority */
        int requestedpriority;
        dispatch_source_t ds_signal, ds_exit;
@@ -629,7 +640,7 @@ memorystatus_assertion_test_demote_frozen()
        });
 
        /* Launch the child process and set the initial properties on it. */
-       child_pid = launch_frozen_background_process();
+       child_pid = launch_background_helper("frozen_background");
        set_memlimits(child_pid, active_limit_mb, inactive_limit_mb, false, false);
        set_assertion_priority(child_pid, requestedpriority, 0x0);
        (void)check_properties(child_pid, requestedpriority, inactive_limit_mb, 0x0, ASSERTION_STATE_IS_SET, "Priority was set");
@@ -671,22 +682,598 @@ T_DECL(budget_replenishment, "budget replenishes properly") {
        length = sizeof(kTestIntervalSecs);
        new_budget_ln = sizeof(new_budget);
        ret = sysctlbyname("vm.memorystatus_freeze_calculate_new_budget", &new_budget, &new_budget_ln, &kTestIntervalSecs, length);
-       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "vm.memorystatus_freeze_calculate_new_budget");
+       T_ASSERT_POSIX_SUCCESS(ret, "vm.memorystatus_freeze_calculate_new_budget");
 
        // Grab the daily budget.
        length = sizeof(memorystatus_freeze_daily_mb_max);
        ret = sysctlbyname("kern.memorystatus_freeze_daily_mb_max", &memorystatus_freeze_daily_mb_max, &length, NULL, 0);
-       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "kern.memorystatus_freeze_daily_mb_max");
+       T_ASSERT_POSIX_SUCCESS(ret, "kern.memorystatus_freeze_daily_mb_max");
 
-       memorystatus_freeze_daily_pages_max = memorystatus_freeze_daily_mb_max * 1024 * 1024 / page_size;
+       memorystatus_freeze_daily_pages_max = memorystatus_freeze_daily_mb_max * 1024UL * 1024UL / page_size;
+       T_LOG("memorystatus_freeze_daily_mb_max %u", memorystatus_freeze_daily_mb_max);
+       T_LOG("memorystatus_freeze_daily_pages_max %u", memorystatus_freeze_daily_pages_max);
+       T_LOG("page_size %u", page_size);
 
        /*
         * We're kTestIntervalSecs past a new interval. Which means we are owed kNumSecondsInDay
         * seconds of budget.
         */
        expected_new_budget_pages = memorystatus_freeze_daily_pages_max;
+       T_LOG("expected_new_budget_pages before %u", expected_new_budget_pages);
+       T_ASSERT_EQ(kTestIntervalSecs, 60 * 60 * 32, "kTestIntervalSecs did not change");
        expected_new_budget_pages += ((kTestIntervalSecs * kFixedPointFactor) / (kNumSecondsInDay)
            * memorystatus_freeze_daily_pages_max) / kFixedPointFactor;
+       T_LOG("expected_new_budget_pages after %u", expected_new_budget_pages);
+       T_LOG("memorystatus_freeze_daily_pages_max after %u", memorystatus_freeze_daily_pages_max);
 
        T_QUIET; T_ASSERT_EQ(new_budget, expected_new_budget_pages, "Calculate new budget behaves correctly.");
 }
+
+
+static bool
+is_proc_in_frozen_list(pid_t pid, char* name, size_t name_len)
+{
+       int bytes_written;
+       bool found = false;
+       global_frozen_procs_t *frozen_procs = malloc(sizeof(global_frozen_procs_t));
+       T_QUIET; T_ASSERT_NOTNULL(frozen_procs, "malloc");
+
+       bytes_written = memorystatus_control(MEMORYSTATUS_CMD_FREEZER_CONTROL, 0, FREEZER_CONTROL_GET_PROCS, frozen_procs, sizeof(global_frozen_procs_t));
+       T_QUIET; T_ASSERT_LE((size_t) bytes_written, sizeof(global_frozen_procs_t), "Didn't overflow buffer");
+       T_QUIET; T_ASSERT_GT(bytes_written, 0, "Wrote someting");
+
+       for (size_t i = 0; i < frozen_procs->gfp_num_frozen; i++) {
+               if (frozen_procs->gfp_procs[i].fp_pid == pid) {
+                       found = true;
+                       strlcpy(name, frozen_procs->gfp_procs[i].fp_name, name_len);
+               }
+       }
+       return found;
+}
+
+static void
+unset_testing_pid(void)
+{
+       int ret;
+       ret = memorystatus_control(MEMORYSTATUS_CMD_SET_TESTING_PID, 0, MEMORYSTATUS_FLAGS_UNSET_TESTING_PID, NULL, 0);
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, 0, "Drop ownership of jetsam snapshot");
+}
+
+static void
+set_testing_pid(void)
+{
+       int ret;
+       ret = memorystatus_control(MEMORYSTATUS_CMD_SET_TESTING_PID, 0, MEMORYSTATUS_FLAGS_SET_TESTING_PID, NULL, 0);
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "Take ownership of jetsam snapshot");
+       T_ATEND(unset_testing_pid);
+}
+
+/*
+ * Retrieve a jetsam snapshot.
+ *
+ * return:
+ *      pointer to snapshot.
+ *
+ *     Caller is responsible for freeing snapshot.
+ */
+static
+memorystatus_jetsam_snapshot_t *
+get_jetsam_snapshot(uint32_t flags, bool empty_allowed)
+{
+       memorystatus_jetsam_snapshot_t * snapshot = NULL;
+       int ret;
+       uint32_t size;
+
+       ret = memorystatus_control(MEMORYSTATUS_CMD_GET_JETSAM_SNAPSHOT, 0, flags, NULL, 0);
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, 0, "Get jetsam snapshot size");
+       size = (uint32_t) ret;
+       if (size == 0 && empty_allowed) {
+               return snapshot;
+       }
+
+       snapshot = (memorystatus_jetsam_snapshot_t*)malloc(size);
+       T_QUIET; T_ASSERT_NOTNULL(snapshot, "Allocate snapshot of size %d", size);
+
+       ret = memorystatus_control(MEMORYSTATUS_CMD_GET_JETSAM_SNAPSHOT, 0, flags, snapshot, size);
+       T_QUIET; T_ASSERT_GT(size, 0, "Get jetsam snapshot");
+
+       if (((size - sizeof(memorystatus_jetsam_snapshot_t)) / sizeof(memorystatus_jetsam_snapshot_entry_t)) != snapshot->entry_count) {
+               T_FAIL("Malformed snapshot: %d! Expected %ld + %zd x %ld = %ld\n", size,
+                   sizeof(memorystatus_jetsam_snapshot_t), snapshot->entry_count, sizeof(memorystatus_jetsam_snapshot_entry_t),
+                   sizeof(memorystatus_jetsam_snapshot_t) + (snapshot->entry_count * sizeof(memorystatus_jetsam_snapshot_entry_t)));
+               if (snapshot) {
+                       free(snapshot);
+               }
+       }
+
+       return snapshot;
+}
+
+/*
+ * Look for the given pid in the snapshot.
+ *
+ * return:
+ *     pointer to pid's entry or NULL if pid is not found.
+ *
+ * Caller has ownership of snapshot before and after call.
+ */
+static
+memorystatus_jetsam_snapshot_entry_t *
+get_jetsam_snapshot_entry(memorystatus_jetsam_snapshot_t *snapshot, pid_t pid)
+{
+       T_QUIET; T_ASSERT_NOTNULL(snapshot, "Got snapshot");
+       for (size_t i = 0; i < snapshot->entry_count; i++) {
+               memorystatus_jetsam_snapshot_entry_t *curr = &(snapshot->entries[i]);
+               if (curr->pid == pid) {
+                       return curr;
+               }
+       }
+
+       return NULL;
+}
+
+static dispatch_source_t
+run_block_after_signal(int sig, dispatch_block_t block)
+{
+       dispatch_source_t ds_signal;
+       signal(sig, SIG_IGN);
+       ds_signal = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, (uintptr_t) sig, 0, dispatch_get_main_queue());
+       T_QUIET; T_ASSERT_NOTNULL(ds_signal, "dispatch_source_create");
+       dispatch_source_set_event_handler(ds_signal, block);
+       return ds_signal;
+}
+
+/*
+ * Launches the child & runs the given block after the child signals.
+ * If exit_with_child is true, the test will exit when the child exits.
+ */
+static void
+test_after_background_helper_launches(bool exit_with_child, const char* variant, dispatch_block_t test_block)
+{
+       dispatch_source_t ds_signal, ds_exit;
+
+       ds_signal = run_block_after_signal(SIGUSR1, test_block);
+       /* Launch the child process. */
+       child_pid = launch_background_helper(variant);
+       /* Listen for exit. */
+       if (exit_with_child) {
+               ds_exit = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, (uintptr_t)child_pid, DISPATCH_PROC_EXIT, dispatch_get_main_queue());
+               dispatch_source_set_event_handler(ds_exit, ^{
+                       int status = 0, code = 0;
+                       pid_t rc = waitpid(child_pid, &status, 0);
+                       T_QUIET; T_ASSERT_EQ(rc, child_pid, "waitpid");
+                       code = WEXITSTATUS(status);
+                       if (code != 0) {
+                               T_LOG("Child exited with error: %s", exit_codes_str[code]);
+                       }
+                       T_QUIET; T_ASSERT_EQ(code, 0, "Child exited cleanly");
+                       T_END;
+               });
+
+               dispatch_activate(ds_exit);
+       }
+       dispatch_activate(ds_signal);
+}
+
+T_DECL(get_frozen_procs, "List processes in the freezer") {
+       skip_if_freezer_is_disabled();
+
+       test_after_background_helper_launches(true, "frozen_background", ^{
+               proc_name_t name;
+               /* Place the child in the idle band so that it gets elevated like a typical app. */
+               move_to_idle_band(child_pid);
+               /* Freeze the process, and check that it's in the list of frozen processes. */
+               freeze_process(child_pid);
+               /* Check */
+               T_QUIET; T_ASSERT_TRUE(is_proc_in_frozen_list(child_pid, name, sizeof(name)), "Found proc in frozen list");
+               T_QUIET; T_EXPECT_EQ_STR(name, "memorystatus_freeze_test", "Proc has correct name");
+               /* Kill the child */
+               T_QUIET; T_ASSERT_POSIX_SUCCESS(kill(child_pid, SIGKILL), "Killed child process");
+               T_END;
+       });
+       dispatch_main();
+}
+
+T_DECL(frozen_to_swap_accounting, "jetsam snapshot has frozen_to_swap accounting") {
+       static const size_t kSnapshotSleepDelay = 5;
+       static const size_t kFreezeToDiskMaxDelay = 60;
+
+       skip_if_freezer_is_disabled();
+
+       test_after_background_helper_launches(true, "frozen_background", ^{
+               memorystatus_jetsam_snapshot_t *snapshot = NULL;
+               memorystatus_jetsam_snapshot_entry_t *child_entry = NULL;
+               /* Place the child in the idle band so that it gets elevated like a typical app. */
+               move_to_idle_band(child_pid);
+               freeze_process(child_pid);
+               /*
+                * Wait until the child's pages get paged out to disk.
+                * If we don't see any pages get sent to disk before kFreezeToDiskMaxDelay seconds,
+                * something is either wrong with the compactor or the accounting.
+                */
+               for (size_t i = 0; i < kFreezeToDiskMaxDelay / kSnapshotSleepDelay; i++) {
+                       snapshot = get_jetsam_snapshot(MEMORYSTATUS_FLAGS_SNAPSHOT_ON_DEMAND, false);
+                       child_entry = get_jetsam_snapshot_entry(snapshot, child_pid);
+                       T_QUIET; T_ASSERT_NOTNULL(child_entry, "Found child in snapshot");
+                       if (child_entry->jse_frozen_to_swap_pages > 0) {
+                               break;
+                       }
+                       free(snapshot);
+                       sleep(kSnapshotSleepDelay);
+               }
+               T_QUIET; T_ASSERT_GT(child_entry->jse_frozen_to_swap_pages, 0ULL, "child has some pages in swap");
+               free(snapshot);
+               /* Kill the child */
+               T_QUIET; T_ASSERT_POSIX_SUCCESS(kill(child_pid, SIGKILL), "Killed child process");
+               T_END;
+       });
+       dispatch_main();
+}
+
+T_DECL(freezer_snapshot, "App kills are recorded in the freezer snapshot") {
+       /* Take ownership of the snapshot to ensure we don't race with another process trying to consume them. */
+       set_testing_pid();
+
+       test_after_background_helper_launches(false, "frozen_background", ^{
+               int ret;
+               memorystatus_jetsam_snapshot_t *snapshot = NULL;
+               memorystatus_jetsam_snapshot_entry_t *child_entry = NULL;
+
+               ret = memorystatus_control(MEMORYSTATUS_CMD_TEST_JETSAM, child_pid, 0, 0, 0);
+               T_ASSERT_POSIX_SUCCESS(ret, "jetsam'd the child");
+
+               snapshot = get_jetsam_snapshot(MEMORYSTATUS_FLAGS_SNAPSHOT_FREEZER, false);
+               T_ASSERT_NOTNULL(snapshot, "Got freezer snapshot");
+               child_entry = get_jetsam_snapshot_entry(snapshot, child_pid);
+               T_QUIET; T_ASSERT_NOTNULL(child_entry, "Child is in freezer snapshot");
+               T_QUIET; T_ASSERT_EQ(child_entry->killed, (unsigned long long) JETSAM_REASON_GENERIC, "Child entry was killed");
+
+               free(snapshot);
+               T_END;
+       });
+       dispatch_main();
+}
+
+T_DECL(freezer_snapshot_consume, "Freezer snapshot is consumed on read") {
+       /* Take ownership of the snapshot to ensure we don't race with another process trying to consume them. */
+       set_testing_pid();
+
+       test_after_background_helper_launches(false, "frozen_background", ^{
+               int ret;
+               memorystatus_jetsam_snapshot_t *snapshot = NULL;
+               memorystatus_jetsam_snapshot_entry_t *child_entry = NULL;
+
+               ret = memorystatus_control(MEMORYSTATUS_CMD_TEST_JETSAM, child_pid, 0, 0, 0);
+               T_ASSERT_POSIX_SUCCESS(ret, "jetsam'd the child");
+
+               snapshot = get_jetsam_snapshot(MEMORYSTATUS_FLAGS_SNAPSHOT_FREEZER, false);
+               T_ASSERT_NOTNULL(snapshot, "Got first freezer snapshot");
+               child_entry = get_jetsam_snapshot_entry(snapshot, child_pid);
+               T_QUIET; T_ASSERT_NOTNULL(child_entry, "Child is in first freezer snapshot");
+
+               snapshot = get_jetsam_snapshot(MEMORYSTATUS_FLAGS_SNAPSHOT_FREEZER, true);
+               if (snapshot != NULL) {
+                       child_entry = get_jetsam_snapshot_entry(snapshot, child_pid);
+                       T_QUIET; T_ASSERT_NULL(child_entry, "Child is not in second freezer snapshot");
+               }
+
+               free(snapshot);
+               T_END;
+       });
+       dispatch_main();
+}
+
+T_DECL(freezer_snapshot_frozen_state, "Frozen state is recorded in freezer snapshot") {
+       skip_if_freezer_is_disabled();
+       /* Take ownership of the snapshot to ensure we don't race with another process trying to consume them. */
+       set_testing_pid();
+
+       test_after_background_helper_launches(false, "frozen_background", ^{
+               int ret;
+               memorystatus_jetsam_snapshot_t *snapshot = NULL;
+               memorystatus_jetsam_snapshot_entry_t *child_entry = NULL;
+
+               move_to_idle_band(child_pid);
+               freeze_process(child_pid);
+
+               ret = memorystatus_control(MEMORYSTATUS_CMD_TEST_JETSAM, child_pid, 0, 0, 0);
+               T_ASSERT_POSIX_SUCCESS(ret, "jetsam'd the child");
+
+               snapshot = get_jetsam_snapshot(MEMORYSTATUS_FLAGS_SNAPSHOT_FREEZER, false);
+               T_ASSERT_NOTNULL(snapshot, "Got freezer snapshot");
+               child_entry = get_jetsam_snapshot_entry(snapshot, child_pid);
+               T_QUIET; T_ASSERT_NOTNULL(child_entry, "Child is in freezer snapshot");
+               T_QUIET; T_ASSERT_TRUE(child_entry->state & kMemorystatusFrozen, "Child entry's frozen bit is set");
+
+               free(snapshot);
+               T_END;
+       });
+       dispatch_main();
+}
+
+T_DECL(freezer_snapshot_thaw_state, "Thaw count is recorded in freezer snapshot") {
+       skip_if_freezer_is_disabled();
+       /* Take ownership of the snapshot to ensure we don't race with another process trying to consume them. */
+       set_testing_pid();
+
+       test_after_background_helper_launches(false, "frozen_background", ^{
+               int ret;
+               memorystatus_jetsam_snapshot_t *snapshot = NULL;
+               memorystatus_jetsam_snapshot_entry_t *child_entry = NULL;
+
+               move_to_idle_band(child_pid);
+               ret = pid_suspend(child_pid);
+               T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "child suspended");
+               freeze_process(child_pid);
+               ret = pid_resume(child_pid);
+               T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "child resumed after freeze");
+
+               ret = memorystatus_control(MEMORYSTATUS_CMD_TEST_JETSAM, child_pid, 0, 0, 0);
+               T_ASSERT_POSIX_SUCCESS(ret, "jetsam'd the child");
+
+               snapshot = get_jetsam_snapshot(MEMORYSTATUS_FLAGS_SNAPSHOT_FREEZER, false);
+               T_ASSERT_NOTNULL(snapshot, "Got freezer snapshot");
+               child_entry = get_jetsam_snapshot_entry(snapshot, child_pid);
+               T_QUIET; T_ASSERT_NOTNULL(child_entry, "Child is in freezer snapshot");
+               T_QUIET; T_ASSERT_TRUE(child_entry->state & kMemorystatusFrozen, "Child entry's frozen bit is still set after thaw");
+               T_QUIET; T_ASSERT_TRUE(child_entry->state & kMemorystatusWasThawed, "Child entry was thawed");
+               T_QUIET; T_ASSERT_EQ(child_entry->jse_thaw_count, 1ULL, "Child entry's thaw count was incremented");
+
+               free(snapshot);
+               T_END;
+       });
+}
+
+T_HELPER_DECL(check_frozen, "Check frozen state", T_META_ASROOT(true)) {
+       int kern_ret;
+       dispatch_source_t ds_signal;
+       __block int is_frozen;
+       /* Set the process to freezable */
+       kern_ret = memorystatus_control(MEMORYSTATUS_CMD_SET_PROCESS_IS_FREEZABLE, getpid(), 1, NULL, 0);
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(kern_ret, "set process is freezable");
+
+       /* We should not be frozen yet. */
+       is_frozen = memorystatus_control(MEMORYSTATUS_CMD_GET_PROCESS_IS_FROZEN, getpid(), 0, NULL, 0);
+       if (is_frozen == -1) {
+               T_LOG("memorystatus_control error: %s", strerror(errno));
+               exit(MEMORYSTATUS_CONTROL_ERROR);
+       }
+       if (is_frozen) {
+               exit(FROZEN_BIT_SET);
+       }
+
+       ds_signal = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGUSR1, 0, dispatch_get_main_queue());
+       if (ds_signal == NULL) {
+               exit(DISPATCH_SOURCE_CREATE_FAILED);
+       }
+
+       dispatch_source_set_event_handler(ds_signal, ^{
+               /* We should now be frozen. */
+               is_frozen = memorystatus_control(MEMORYSTATUS_CMD_GET_PROCESS_IS_FROZEN, getpid(), 0, NULL, 0);
+               if (is_frozen == -1) {
+                       T_LOG("memorystatus_control error: %s", strerror(errno));
+                       exit(MEMORYSTATUS_CONTROL_ERROR);
+               }
+               if (!is_frozen) {
+                       exit(FROZEN_BIT_NOT_SET);
+               }
+               exit(SUCCESS);
+       });
+       dispatch_activate(ds_signal);
+
+       sig_t sig_ret = signal(SIGUSR1, SIG_IGN);
+       T_QUIET; T_WITH_ERRNO; T_ASSERT_NE(sig_ret, SIG_ERR, "signal(SIGUSR1, SIG_IGN)");
+
+       /* Signal to our parent that we can be frozen */
+       if (kill(getppid(), SIGUSR1) != 0) {
+               T_LOG("Unable to signal to parent process!");
+               exit(SIGNAL_TO_PARENT_FAILED);
+       }
+
+       dispatch_main();
+}
+
+T_DECL(memorystatus_get_process_is_frozen, "MEMORYSTATUS_CMD_GET_PROCESS_IS_FROZEN returns correct state") {
+       skip_if_freezer_is_disabled();
+
+       test_after_background_helper_launches(true, "check_frozen", ^{
+               int ret;
+               /* Freeze the child, resume it, and signal it to check its state */
+               move_to_idle_band(child_pid);
+               ret = pid_suspend(child_pid);
+               T_ASSERT_POSIX_SUCCESS(ret, "child suspended");
+               freeze_process(child_pid);
+               ret = pid_resume(child_pid);
+               T_ASSERT_POSIX_SUCCESS(ret, "child resumed after freeze");
+
+               kill(child_pid, SIGUSR1);
+               /* The child will checks its own frozen state & exit. */
+       });
+       dispatch_main();
+}
+
+static unsigned int freeze_pages_min_old;
+static int throttle_enabled_old;
+static void cleanup_memorystatus_freeze_top_process() {
+       sysctlbyname("kern.memorystatus_freeze_pages_min", NULL, NULL, &freeze_pages_min_old, sizeof(freeze_pages_min_old));
+       sysctlbyname("kern.memorystatus_freeze_throttle_enabled", NULL, NULL, &throttle_enabled_old, sizeof(throttle_enabled_old));
+}
+
+#define P_MEMSTAT_FROZEN 0x00000002
+T_DECL(memorystatus_freeze_top_process, "memorystatus_freeze_top_process chooses the correct process",
+    T_META_ASROOT(true),
+    T_META_REQUIRES_SYSCTL_EQ("kern.development", 1),
+    T_META_REQUIRES_SYSCTL_EQ("vm.freeze_enabled", 1)) {
+       int32_t memorystatus_freeze_band = 0;
+       size_t memorystatus_freeze_band_size = sizeof(memorystatus_freeze_band);
+       size_t freeze_pages_min_size = sizeof(freeze_pages_min_old);
+       unsigned int freeze_pages_min_new = 0;
+       size_t throttle_enabled_old_size = sizeof(throttle_enabled_old);
+       int throttle_enabled_new = 1;
+       __block errno_t ret;
+       __block int maxproc;
+       size_t maxproc_size = sizeof(maxproc);
+
+       ret = sysctlbyname("kern.maxproc", &maxproc, &maxproc_size, NULL, 0);
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "kern.maxproc");
+       sysctlbyname("kern.memorystatus_freeze_jetsam_band", &memorystatus_freeze_band, &memorystatus_freeze_band_size, NULL, 0);
+
+       /* Set min pages to 0 and disable the budget to ensure we can always freeze the child. */
+       ret = sysctlbyname("kern.memorystatus_freeze_pages_min", &freeze_pages_min_old, &freeze_pages_min_size, &freeze_pages_min_new, sizeof(freeze_pages_min_new));
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "set kern.memorystatus_freeze_pages_min");
+       ret = sysctlbyname("kern.memorystatus_freeze_throttle_enabled", &throttle_enabled_old, &throttle_enabled_old_size, &throttle_enabled_new, sizeof(throttle_enabled_new));
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "set kern.memorystatus_freeze_throttle_enabled");
+       T_ATEND(cleanup_memorystatus_freeze_top_process);
+       /* Take ownership of the freezer probabilities for the duration of the test so that we don't race with dasd. */
+       set_testing_pid();
+       test_after_background_helper_launches(true, "frozen_background", ^{
+               int32_t child_band = JETSAM_PRIORITY_DEFAULT;
+               /* Place the child in the idle band so that it gets elevated like a typical app. */
+               move_to_idle_band(child_pid);
+               ret = pid_suspend(child_pid);
+               T_ASSERT_POSIX_SUCCESS(ret, "child suspended");
+
+               size_t buffer_len = sizeof(memorystatus_properties_entry_v1_t) * (size_t) maxproc;
+               memorystatus_properties_entry_v1_t *properties_list = malloc(buffer_len);
+               T_QUIET; T_ASSERT_NOTNULL(properties_list, "malloc properties array");
+               size_t properties_list_len = 0;
+               /* The child needs to age down into the idle band before it's eligible to be frozen. */
+               T_LOG("Waiting for child to age into the idle band.");
+               while (child_band != JETSAM_PRIORITY_IDLE) {
+                       memset(properties_list, 0, buffer_len);
+                       properties_list_len = 0;
+                       memorystatus_jetsam_snapshot_t *snapshot = get_jetsam_snapshot(MEMORYSTATUS_FLAGS_SNAPSHOT_ON_DEMAND, false);
+
+                       bool found = false;
+                       for (size_t i = 0; i < snapshot->entry_count; i++) {
+                               memorystatus_jetsam_snapshot_entry_t *snapshot_entry = &snapshot->entries[i];
+                               if (snapshot_entry->priority <= memorystatus_freeze_band && !snapshot_entry->killed) {
+                                       pid_t pid = snapshot_entry->pid;
+                                       memorystatus_properties_entry_v1_t *property_entry = &properties_list[properties_list_len++];
+                                       property_entry->version = 1;
+                                       property_entry->pid = pid;
+                                       if (pid == child_pid) {
+                                               found = true;
+                                               property_entry->use_probability = 1;
+                                               child_band = snapshot_entry->priority;
+                                       } else {
+                                               property_entry->use_probability = 0;
+                                       }
+                                       strncpy(property_entry->proc_name, snapshot_entry->name, MAXCOMLEN);
+                                       property_entry->proc_name[MAXCOMLEN] = '\0';
+                               }
+                       }
+                       T_QUIET; T_ASSERT_TRUE(found, "Child is in on demand snapshot");
+                       free(snapshot);
+               }
+               ret = memorystatus_control(MEMORYSTATUS_CMD_GRP_SET_PROPERTIES, 0, MEMORYSTATUS_FLAGS_GRP_SET_PROBABILITY, properties_list, sizeof(memorystatus_properties_entry_v1_t) * properties_list_len);
+               T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "MEMORYSTATUS_FLAGS_GRP_SET_PROBABILITY");
+               free(properties_list);
+               int val = 1;
+               ret = sysctlbyname("vm.memorystatus_freeze_top_process", NULL, NULL, &val, sizeof(val));
+               T_ASSERT_POSIX_SUCCESS(ret, "freeze_top_process");
+               /* Verify that the process was frozen. */
+               memorystatus_jetsam_snapshot_t *snapshot = get_jetsam_snapshot(MEMORYSTATUS_FLAGS_SNAPSHOT_ON_DEMAND, false);
+               memorystatus_jetsam_snapshot_entry_t *entry = get_jetsam_snapshot_entry(snapshot, child_pid);
+               T_ASSERT_NOTNULL(entry, "child is in snapshot");
+               if (!(entry->state & P_MEMSTAT_FROZEN)) {
+                       T_LOG("Not frozen. Skip reason: %d", entry->jse_freeze_skip_reason);
+               }
+               T_ASSERT_TRUE(entry->state & P_MEMSTAT_FROZEN, "child is frozen");
+               free(snapshot);
+               ret = pid_resume(child_pid);
+               T_ASSERT_POSIX_SUCCESS(ret, "child resumed after freeze");
+
+               /* Kill the child */
+               T_QUIET; T_ASSERT_POSIX_SUCCESS(kill(child_pid, SIGKILL), "Killed child process");
+               T_END;
+       });
+       dispatch_main();
+}
+
+static int
+memorystatus_freezer_thaw_percentage(void)
+{
+       int val;
+       size_t size = sizeof(val);
+       int ret = sysctlbyname("kern.memorystatus_freezer_thaw_percentage", &val, &size, NULL, 0);
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "failed to query kern.memorystatus_freezer_thaw_percentage");
+       return val;
+}
+
+static void
+reset_interval(void)
+{
+       uint32_t freeze_daily_budget_mb = 0;
+       size_t size = sizeof(freeze_daily_budget_mb);
+       int ret;
+       uint64_t new_budget;
+       ret = sysctlbyname("kern.memorystatus_freeze_daily_mb_max", &freeze_daily_budget_mb, &size, NULL, 0);
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "failed to query kern.memorystatus_freeze_daily_mb_max");
+       new_budget = (freeze_daily_budget_mb * (1UL << 20) / vm_page_size);
+       ret = sysctlbyname("kern.memorystatus_freeze_budget_pages_remaining", NULL, NULL, &new_budget, sizeof(new_budget));
+       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "failed to set kern.memorystatus_freeze_budget_pages_remaining");
+}
+
+static pid_t second_child;
+static void
+cleanup_memorystatus_freezer_thaw_percentage(void)
+{
+       kill(second_child, SIGKILL);
+}
+
+T_DECL(memorystatus_freezer_thaw_percentage, "memorystatus_freezer_thaw_percentage updates correctly",
+    T_META_ASROOT(true),
+    T_META_REQUIRES_SYSCTL_EQ("kern.development", 1),
+    T_META_REQUIRES_SYSCTL_EQ("vm.freeze_enabled", 1)) {
+       __block dispatch_source_t first_signal_block;
+       /* Take ownership of the freezer probabilities for the duration of the test so that nothing new gets frozen by dasd. */
+       set_testing_pid();
+       reset_interval();
+
+       /* Spawn one child that will remain frozen throughout the whole test & another that will be thawed. */
+       first_signal_block = run_block_after_signal(SIGUSR1, ^{
+               move_to_idle_band(second_child);
+               __block int ret = pid_suspend(second_child);
+               T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "child suspended");
+               freeze_process(second_child);
+               T_QUIET; T_ASSERT_EQ(memorystatus_freezer_thaw_percentage(), 0, "thaw percentage is still 0 after freeze");
+               dispatch_source_cancel(first_signal_block);
+               test_after_background_helper_launches(true, "frozen_background", ^{
+                       reset_interval();
+                       T_QUIET; T_ASSERT_EQ(memorystatus_freezer_thaw_percentage(), 0, "new interval starts with a thaw percentage of 0");
+                       move_to_idle_band(child_pid);
+                       ret = pid_suspend(child_pid);
+                       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "child suspended");
+                       freeze_process(child_pid);
+                       ret = pid_resume(child_pid);
+                       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "child resumed after freeze");
+                       int percentage_after_thaw = memorystatus_freezer_thaw_percentage();
+                       T_QUIET; T_ASSERT_GT(percentage_after_thaw, 0, "thaw percentage is higher after thaw");
+
+                       ret = pid_suspend(child_pid);
+                       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "child suspended");
+                       freeze_process(child_pid);
+                       ret = pid_resume(child_pid);
+                       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "child resumed after freeze");
+                       T_QUIET; T_ASSERT_EQ(memorystatus_freezer_thaw_percentage(), percentage_after_thaw, "thaw percentage is unchanged after second thaw");
+
+                       ret = pid_suspend(child_pid);
+                       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "child suspended");
+                       freeze_process(child_pid);
+                       reset_interval();
+                       T_QUIET; T_ASSERT_EQ(memorystatus_freezer_thaw_percentage(), 0, "new interval starts with a 0 thaw percentage");
+                       ret = pid_resume(child_pid);
+                       T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "child resumed after freeze");
+                       T_QUIET; T_ASSERT_GT(memorystatus_freezer_thaw_percentage(), 0, "thaw percentage goes back up in new interval");
+
+                       T_QUIET; T_ASSERT_POSIX_SUCCESS(kill(child_pid, SIGKILL), "failed to kill child");
+                       T_END;
+               });
+       });
+
+       second_child = launch_background_helper("frozen_background");
+       T_ATEND(cleanup_memorystatus_freezer_thaw_percentage);
+       dispatch_activate(first_signal_block);
+       dispatch_main();
+}