// Copyright (c) 2024 Apple Inc. All rights reserved. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "test_utils.h" #define CONFIG_THREAD_GROUPS 1 typedef void *cluster_type_t; #include "../../osfmk/kern/thread_group.h" #include "thread_group_flags_workload_config.h" T_GLOBAL_META(T_META_NAMESPACE("xnu.scheduler"), T_META_RADAR_COMPONENT_NAME("xnu"), T_META_RADAR_COMPONENT_VERSION("scheduler"), T_META_REQUIRES_SYSCTL_EQ("kern.thread_groups_supported", 1)); static void workload_config_load(void) { int ret; size_t len = 0; ret = sysctlbyname("kern.workload_config", NULL, &len, sched_thread_group_flags_workload_config_plist, sched_thread_group_flags_workload_config_plist_len); if (ret == -1 && errno == ENOENT) { T_SKIP("kern.workload_config failed"); } T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "kern.workload_config"); } static void workload_config_cleanup(void) { size_t len = 0; sysctlbyname("kern.workload_config", NULL, &len, "", 1); } static void set_work_interval_id(work_interval_t *handle, uint32_t work_interval_flags, char *workload_id) { int ret; mach_port_t port = MACH_PORT_NULL; ret = work_interval_copy_port(*handle, &port); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "work_interval_copy_port"); struct work_interval_workload_id_params wlid_params = { .wlidp_flags = WORK_INTERVAL_WORKLOAD_ID_HAS_ID, .wlidp_wicreate_flags = work_interval_flags, .wlidp_name = (uintptr_t)workload_id, }; ret = __work_interval_ctl(WORK_INTERVAL_OPERATION_SET_WORKLOAD_ID, port, &wlid_params, sizeof(wlid_params)); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "WORK_INTERVAL_OPERATION_SET_WORKLOAD_ID"); } static void make_work_interval(work_interval_t *handle, uint32_t work_type_flags, char *workload_id) { int ret; uint32_t work_interval_flags = WORK_INTERVAL_FLAG_JOINABLE | WORK_INTERVAL_FLAG_GROUP | work_type_flags; ret = work_interval_create(handle, work_interval_flags); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "work_interval_create"); if (work_type_flags & WORK_INTERVAL_FLAG_HAS_WORKLOAD_ID) { set_work_interval_id(handle, work_interval_flags, workload_id); } } static uint64_t get_thread_group_id(void) { int ret; uint64_t tg_id; size_t tg_id_len = sizeof(tg_id); ret = sysctlbyname("kern.thread_group_id", &tg_id, &tg_id_len, NULL, 0); T_QUIET; T_WITH_ERRNO; T_ASSERT_POSIX_SUCCESS(ret, "kern.thread_group_id"); return tg_id; } struct thread_data { work_interval_t wi_handle; uint64_t tg_id; }; static void * join_workload_fn(void *arg) { int ret; struct thread_data *data = (struct thread_data *)arg; uint64_t old_tg_id = get_thread_group_id(); /* Join the thread group associated with the work interval handle */ ret = work_interval_join(data->wi_handle); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "work_interval_join"); data->tg_id = get_thread_group_id(); T_LOG("Joined TG %llx", data->tg_id); T_QUIET; T_EXPECT_NE(data->tg_id, old_tg_id, "Thread failed to join new TG"); return NULL; } static pthread_t * start_threads(void *(*func)(void *), struct thread_data *datas, int num_threads) { int ret; pthread_t *threads = (pthread_t *)malloc(sizeof(pthread_t) * num_threads); pthread_attr_t attr; ret = pthread_attr_init(&attr); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "pthread_attr_init"); for (int i = 0; i < num_threads; i++) { struct sched_param param = { .sched_priority = 31 }; ret = pthread_attr_setschedparam(&attr, ¶m); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "pthread_attr_setschedparam"); ret = pthread_create(&threads[i], &attr, func, (void *)&datas[i]); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "pthread_create"); } return threads; } static void start_test(ktrace_session_t session, pthread_t *threads, int num_threads) { dispatch_async(dispatch_get_main_queue(), ^{ /* Wait for threads to finish, as last test action */ for (int i = 0; i < num_threads; i++) { int ret = pthread_join(threads[i], NULL); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "pthread_join"); } ktrace_end(session, 0); }); dispatch_main(); } static char *trace_location = NULL; static void delete_trace_file(void) { if (T_FAILCOUNT == 0) { T_LOG("Test passed, so deleting \"%s\" to save memory", trace_location); int ret; /* Delete trace file in order to reclaim disk space on the test device */ ret = remove(trace_location); T_QUIET; T_WITH_ERRNO; T_ASSERT_POSIX_SUCCESS(ret, "remove trace file"); } } static char * make_ktrace_filepath(char *short_name) { int ret; char *filepath = (char *)malloc(sizeof(char) * MAXPATHLEN); snprintf(filepath, MAXPATHLEN, "%s/%s.ktrace", dt_tmpdir(), short_name); ret = remove(filepath); T_QUIET; T_WITH_ERRNO; T_ASSERT_TRUE((ret == 0) || (errno == ENOENT), "remove"); return filepath; } static const int num_workload_ids = 5; static char *workload_ids[num_workload_ids] = { "com.test.myapp.efficient", "com.test.myapp.best_effort", "com.test.myapp.application", "com.test.myapp.critical", "com.test.myapp.shared_flags", }; static uint64_t expected_tg_flags[num_workload_ids] = { THREAD_GROUP_FLAGS_EFFICIENT, THREAD_GROUP_FLAGS_BEST_EFFORT, THREAD_GROUP_FLAGS_APPLICATION, THREAD_GROUP_FLAGS_CRITICAL, #if TARGET_OS_XR THREAD_GROUP_FLAGS_MANAGED | THREAD_GROUP_FLAGS_STRICT_TIMERS | THREAD_GROUP_FLAGS_APPLICATION, #else /* !TARGET_OS_XR */ THREAD_GROUP_FLAGS_APPLICATION, #endif /* !TARGET_OS_XR */ }; static int tg_id_to_index(struct thread_data *datas, int num_datas, uint64_t tg_id) { int index = -1; for (int i = 0; i < num_datas; i++) { if (tg_id == datas[i].tg_id) { index = i; break; } } return index; } static void search_for_workload_id_tg_flags_tracepoints(char *trace_path, int num_workload_ids, struct thread_data *datas) { __block int ret; trace_location = trace_path; T_ATEND(delete_trace_file); ktrace_session_t read_session = ktrace_session_create(); ret = ktrace_set_file(read_session, trace_path); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_set_file"); __block int num_validated_new_tgs = 0; ktrace_events_single(read_session, MACHDBG_CODE(DBG_MACH_THREAD_GROUP, MACH_THREAD_GROUP_NEW), ^(ktrace_event_t e) { ret = ktrace_print_trace_point(stdout, read_session, e, KTP_KIND_CSV, KTP_FLAG_WALLTIME | KTP_FLAG_THREADNAME | KTP_FLAG_PID | KTP_FLAG_EVENTNAME | KTP_FLAG_EXECNAME); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "ktrace_print_trace_point output"); printf("\n"); // Flush output from ktrace_print_trace_point uint64_t tg_id = e->arg1; uint64_t tg_flags = e->arg2; int workload_ind = tg_id_to_index(datas, num_workload_ids, tg_id); if (workload_ind != -1) { T_LOG("MACH_THREAD_GROUP_NEW tracepoint from TG %llx with flags %llx, expecting %llx", tg_id, tg_flags, expected_tg_flags[workload_ind]); T_EXPECT_EQ(tg_flags, expected_tg_flags[workload_ind], "Correct new TG flags for \"%s\"", workload_ids[workload_ind]); num_validated_new_tgs++; } }); __block int num_validated_flags = 0; ktrace_events_single(read_session, MACHDBG_CODE(DBG_MACH_THREAD_GROUP, MACH_THREAD_GROUP_FLAGS), ^(ktrace_event_t e) { ret = ktrace_print_trace_point(stdout, read_session, e, KTP_KIND_CSV, KTP_FLAG_WALLTIME | KTP_FLAG_THREADNAME | KTP_FLAG_PID | KTP_FLAG_EVENTNAME | KTP_FLAG_EXECNAME); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "ktrace_print_trace_point output"); printf("\n"); // Flush output from ktrace_print_trace_point uint64_t tg_id = e->arg1; uint64_t tg_flags = e->arg2; int workload_ind = tg_id_to_index(datas, num_workload_ids, tg_id); if (workload_ind != -1) { T_LOG("MACH_THREAD_GROUP_FLAGS tracepoint from TG %llx with flags %llx, expecting %llx", tg_id, tg_flags, expected_tg_flags[workload_ind]); T_EXPECT_EQ(tg_flags, expected_tg_flags[workload_ind], "Correct TG flags for \"%s\"", workload_ids[workload_ind]); T_QUIET; T_EXPECT_EQ(e->arg3, 0ULL, "tracepoint not dropped at TG creation time"); num_validated_flags++; } }); __block int num_validated_joins = 0; ktrace_events_single(read_session, MACHDBG_CODE(DBG_MACH_THREAD_GROUP, MACH_THREAD_GROUP_SET), ^(ktrace_event_t e) { uint64_t new_tg_id = e->arg2; int workload_ind = tg_id_to_index(datas, num_workload_ids, new_tg_id); if (workload_ind != -1) { ret = ktrace_print_trace_point(stdout, read_session, e, KTP_KIND_CSV, KTP_FLAG_WALLTIME | KTP_FLAG_THREADNAME | KTP_FLAG_PID | KTP_FLAG_EVENTNAME | KTP_FLAG_EXECNAME); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "ktrace_print_trace_point output"); printf("\n"); // Flush output from ktrace_print_trace_point T_LOG("MACH_THREAD_GROUP_SET tracepoint for joining TG %llx", new_tg_id); num_validated_joins++; } }); ktrace_set_completion_handler(read_session, ^{ T_EXPECT_EQ(num_validated_new_tgs, num_workload_ids, "Found all expected MACH_THREAD_GROUP_NEW tracepoints"); T_EXPECT_EQ(num_validated_flags, num_workload_ids, "Found all expected MACH_THREAD_GROUP_FLAGS tracepoints"); T_EXPECT_EQ(num_validated_joins, num_workload_ids, "Found all expected MACH_THREAD_GROUP_SET tracepoints"); T_END; }); ret = ktrace_start(read_session, dispatch_get_main_queue()); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_start"); } static const char *THREAD_GROUP_FILTER = "S0x01A6"; T_DECL(thread_group_flags_from_workload_properties, "Verify that workload properties correctly propagate thread group flags", T_META_ASROOT(true)) { int ret; T_ATEND(workload_config_cleanup); workload_config_load(); ktrace_session_t session = ktrace_session_create(); char *filepath = make_ktrace_filepath("thread_group_flags_from_workload_properties"); ret = ktrace_events_filter(session, THREAD_GROUP_FILTER, ^(__unused ktrace_event_t event){}); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_events_filter"); __block struct thread_data *datas = (struct thread_data *)calloc(num_workload_ids, sizeof(struct thread_data)); ktrace_set_completion_handler(session, ^{ search_for_workload_id_tg_flags_tracepoints(filepath, num_workload_ids, datas); }); ret = ktrace_start_writing_path(session, filepath, 0); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_start_writing_path"); T_LOG("Ktrace file being written to %s", filepath); /* Create a work interval for each test workload id */ work_interval_t wi_handles[num_workload_ids]; for (int w = 0; w < num_workload_ids; w++) { make_work_interval(&wi_handles[w], WORK_INTERVAL_TYPE_DEFAULT | WORK_INTERVAL_FLAG_HAS_WORKLOAD_ID, workload_ids[w]); } for (int i = 0; i < num_workload_ids; i++) { datas[i].wi_handle = wi_handles[i]; } __block pthread_t *threads = start_threads(join_workload_fn, datas, num_workload_ids); start_test(session, threads, num_workload_ids); } static void * join_leave_pid_based(void *arg) { int ret; struct thread_data *data = (struct thread_data *)arg; data->tg_id = get_thread_group_id(); ret = setpriority(PRIO_DARWIN_CARPLAY_MODE, 0, PRIO_DARWIN_CARPLAY_MODE_ON); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "set_priority(PRIO_DARWIN_CARPLAY_MODE_ON)"); ret = setpriority(PRIO_DARWIN_CARPLAY_MODE, 0, PRIO_DARWIN_CARPLAY_MODE_OFF); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "set_priority(PRIO_DARWIN_CARPLAY_MODE_OFF)"); ret = setpriority(PRIO_DARWIN_GAME_MODE, 0, PRIO_DARWIN_GAME_MODE_ON); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "set_priority(PRIO_DARWIN_GAME_MODE_ON)"); ret = setpriority(PRIO_DARWIN_GAME_MODE, 0, PRIO_DARWIN_GAME_MODE_OFF); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "set_priority(PRIO_DARWIN_GAME_MODE_OFF)"); T_EXPECT_EQ(data->tg_id, get_thread_group_id(), "Unchanged TG"); return NULL; } static void search_for_pid_based_tg_flags_tracepoints(char *trace_path, int num_threads, struct thread_data *datas) { __block int ret; trace_location = trace_path; T_ATEND(delete_trace_file); ktrace_session_t read_session = ktrace_session_create(); ret = ktrace_set_file(read_session, trace_path); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_set_file"); __block int tracepoint_idx = 0; ktrace_events_single(read_session, MACHDBG_CODE(DBG_MACH_THREAD_GROUP, MACH_THREAD_GROUP_FLAGS), ^(ktrace_event_t e) { ret = ktrace_print_trace_point(stdout, read_session, e, KTP_KIND_CSV, KTP_FLAG_WALLTIME | KTP_FLAG_THREADNAME | KTP_FLAG_PID | KTP_FLAG_EVENTNAME | KTP_FLAG_EXECNAME); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "ktrace_print_trace_point output"); printf("\n"); // Flush output from ktrace_print_trace_point uint64_t tg_id = e->arg1; uint64_t new_tg_flags = e->arg2; uint64_t old_tg_flags = e->arg3; int data = tg_id_to_index(datas, num_workload_ids, tg_id); if (data != -1) { T_LOG("MACH_THREAD_GROUP_FLAGS tracepoint from TG %llx with new flags %llx from old flags %llx", tg_id, new_tg_flags, old_tg_flags); bool had_carplay = old_tg_flags & THREAD_GROUP_FLAGS_CARPLAY_MODE; bool has_carplay = new_tg_flags & THREAD_GROUP_FLAGS_CARPLAY_MODE; bool had_gamemode = old_tg_flags & THREAD_GROUP_FLAGS_GAME_MODE; bool has_gamemode = new_tg_flags & THREAD_GROUP_FLAGS_GAME_MODE; switch (tracepoint_idx) { case 0: T_QUIET; T_EXPECT_TRUE(!had_gamemode && !has_gamemode, "Game Mode on"); T_EXPECT_TRUE(!had_carplay && has_carplay, "Correct flags for Car Play"); break; case 1: T_QUIET; T_EXPECT_TRUE(!had_gamemode && !has_gamemode, "Game Mode on"); T_EXPECT_TRUE(had_carplay && !has_carplay, "Correct flags for disabled Car Play"); break; case 2: T_QUIET; T_EXPECT_TRUE(!had_carplay && !has_carplay, "Car Play on"); T_EXPECT_TRUE(!had_gamemode && has_gamemode, "Correct flags for Game Mode"); break; case 3: T_QUIET; T_EXPECT_TRUE(!had_carplay && !has_carplay, "Car Play on"); T_EXPECT_TRUE(had_gamemode && !has_gamemode, "Correct flags for disabled Game Mode"); break; } T_QUIET; T_EXPECT_FALSE(new_tg_flags & THREAD_GROUP_FLAGS_EFFICIENT, "Test runner TG should not be efficient"); tracepoint_idx++; } }); ktrace_set_completion_handler(read_session, ^{ T_EXPECT_EQ(tracepoint_idx, 4, "Found all expected MACH_THREAD_GROUP_FLAGS tracepoints"); T_END; }); ret = ktrace_start(read_session, dispatch_get_main_queue()); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_start"); } T_DECL(thread_group_flags_from_pid_interfaces, "Verify that Car Play and Game Mode correctly propagate thread group flags", T_META_ASROOT(true)) { int ret; int num_threads = 1; ktrace_session_t session = ktrace_session_create(); char *filepath = make_ktrace_filepath("thread_group_flags_from_pid_interfaces"); ret = ktrace_events_filter(session, THREAD_GROUP_FILTER, ^(__unused ktrace_event_t event){}); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_events_filter"); __block struct thread_data *datas = (struct thread_data *)calloc(num_threads, sizeof(struct thread_data)); ktrace_set_completion_handler(session, ^{ search_for_pid_based_tg_flags_tracepoints(filepath, num_threads, datas); }); ret = ktrace_start_writing_path(session, filepath, 0); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_start_writing_path"); T_LOG("Ktrace file being written to %s", filepath); __block pthread_t *threads = start_threads(join_leave_pid_based, datas, num_threads); start_test(session, threads, num_threads); }