| 1 | /* |
| 2 | * Copyright (c) 2000-2021 Apple Inc. All rights reserved. |
| 3 | * |
| 4 | * @APPLE_OSREFERENCE_LICENSE_HEADER_START@ |
| 5 | * |
| 6 | * This file contains Original Code and/or Modifications of Original Code |
| 7 | * as defined in and that are subject to the Apple Public Source License |
| 8 | * Version 2.0 (the 'License'). You may not use this file except in |
| 9 | * compliance with the License. The rights granted to you under the License |
| 10 | * may not be used to create, or enable the creation or redistribution of, |
| 11 | * unlawful or unlicensed copies of an Apple operating system, or to |
| 12 | * circumvent, violate, or enable the circumvention or violation of, any |
| 13 | * terms of an Apple operating system software license agreement. |
| 14 | * |
| 15 | * Please obtain a copy of the License at |
| 16 | * http://www.opensource.apple.com/apsl/ and read it before using this file. |
| 17 | * |
| 18 | * The Original Code and all software distributed under the License are |
| 19 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER |
| 20 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, |
| 21 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, |
| 22 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. |
| 23 | * Please see the License for the specific language governing rights and |
| 24 | * limitations under the License. |
| 25 | * |
| 26 | * @APPLE_OSREFERENCE_LICENSE_HEADER_END@ |
| 27 | */ |
| 28 | |
| 29 | /* |
| 30 | * Telemetry from the VM is usually colected at a daily cadence. |
| 31 | * All of those events are in this file along with a single thread |
| 32 | * call for reporting them. |
| 33 | * |
| 34 | * NB: The freezer subsystem has its own telemetry based on its budget interval |
| 35 | * so it's not included here. |
| 36 | */ |
| 37 | |
| 38 | #include <kern/thread_call.h> |
| 39 | #include <libkern/coreanalytics/coreanalytics.h> |
| 40 | #include <os/log.h> |
| 41 | #include <vm/vm_page.h> |
| 42 | #if CONFIG_EXCLAVES |
| 43 | #include <kern/exclaves_memory.h> |
| 44 | #endif /* CONFIG_EXCLAVES */ |
| 45 | |
| 46 | #include "vm_compressor_backing_store.h" |
| 47 | |
| 48 | void vm_analytics_tick(void *arg0, void *arg1); |
| 49 | |
| 50 | #define ANALYTICS_PERIOD_HOURS (24ULL) |
| 51 | |
| 52 | static thread_call_t vm_analytics_thread_call; |
| 53 | |
| 54 | CA_EVENT(vm_swapusage, |
| 55 | CA_INT, max_alloced, |
| 56 | CA_INT, max_used, |
| 57 | CA_INT, trial_deployment_id, |
| 58 | CA_STATIC_STRING(CA_UUID_LEN), trial_treatment_id, |
| 59 | CA_STATIC_STRING(CA_UUID_LEN), trial_experiment_id); |
| 60 | |
| 61 | CA_EVENT(mlock_failures, |
| 62 | CA_INT, over_global_limit, |
| 63 | CA_INT, over_user_limit, |
| 64 | CA_INT, trial_deployment_id, |
| 65 | CA_STATIC_STRING(CA_UUID_LEN), trial_treatment_id, |
| 66 | CA_STATIC_STRING(CA_UUID_LEN), trial_experiment_id); |
| 67 | |
| 68 | /* |
| 69 | * NB: It's a good practice to include these trial |
| 70 | * identifiers in all of our events so that we can |
| 71 | * measure the impact of any A/B tests on these metrics. |
| 72 | */ |
| 73 | extern uuid_string_t trial_treatment_id; |
| 74 | extern uuid_string_t trial_experiment_id; |
| 75 | extern int trial_deployment_id; |
| 76 | |
| 77 | static void |
| 78 | add_trial_uuids(char *treatment_id, char *experiment_id) |
| 79 | { |
| 80 | strlcpy(dst: treatment_id, src: trial_treatment_id, CA_UUID_LEN); |
| 81 | strlcpy(dst: experiment_id, src: trial_experiment_id, CA_UUID_LEN); |
| 82 | } |
| 83 | |
| 84 | static void |
| 85 | report_vm_swapusage() |
| 86 | { |
| 87 | uint64_t max_alloced, max_used; |
| 88 | ca_event_t event = CA_EVENT_ALLOCATE(vm_swapusage); |
| 89 | CA_EVENT_TYPE(vm_swapusage) * e = event->data; |
| 90 | |
| 91 | vm_swap_reset_max_segs_tracking(alloced_max: &max_alloced, used_max: &max_used); |
| 92 | e->max_alloced = max_alloced; |
| 93 | e->max_used = max_used; |
| 94 | add_trial_uuids(treatment_id: e->trial_treatment_id, experiment_id: e->trial_experiment_id); |
| 95 | e->trial_deployment_id = trial_deployment_id; |
| 96 | CA_EVENT_SEND(event); |
| 97 | } |
| 98 | |
| 99 | static void |
| 100 | report_mlock_failures() |
| 101 | { |
| 102 | ca_event_t event = CA_EVENT_ALLOCATE(mlock_failures); |
| 103 | CA_EVENT_TYPE(mlock_failures) * e = event->data; |
| 104 | |
| 105 | e->over_global_limit = os_atomic_load_wide(&vm_add_wire_count_over_global_limit, relaxed); |
| 106 | e->over_user_limit = os_atomic_load_wide(&vm_add_wire_count_over_user_limit, relaxed); |
| 107 | |
| 108 | os_atomic_store_wide(&vm_add_wire_count_over_global_limit, 0, relaxed); |
| 109 | os_atomic_store_wide(&vm_add_wire_count_over_user_limit, 0, relaxed); |
| 110 | |
| 111 | add_trial_uuids(treatment_id: e->trial_treatment_id, experiment_id: e->trial_experiment_id); |
| 112 | e->trial_deployment_id = trial_deployment_id; |
| 113 | CA_EVENT_SEND(event); |
| 114 | } |
| 115 | |
| 116 | #if XNU_TARGET_OS_WATCH |
| 117 | CA_EVENT(compressor_age, |
| 118 | CA_INT, hour1, |
| 119 | CA_INT, hour6, |
| 120 | CA_INT, hour12, |
| 121 | CA_INT, hour24, |
| 122 | CA_INT, hour36, |
| 123 | CA_INT, hour48, |
| 124 | CA_INT, hourMax, |
| 125 | CA_INT, trial_deployment_id, |
| 126 | CA_STATIC_STRING(CA_UUID_LEN), trial_treatment_id, |
| 127 | CA_STATIC_STRING(CA_UUID_LEN), trial_experiment_id); |
| 128 | |
| 129 | /** |
| 130 | * Compressor age bucket descriptor. |
| 131 | */ |
| 132 | typedef struct { |
| 133 | /* Number of segments in this bucket. */ |
| 134 | uint64_t count; |
| 135 | /* The bucket's lower bound (inclusive) */ |
| 136 | uint64_t lower; |
| 137 | /* The bucket's upper bound (exclusive) */ |
| 138 | uint64_t upper; |
| 139 | } c_reporting_bucket_t; |
| 140 | #define C_REPORTING_BUCKETS_MAX (UINT64_MAX) |
| 141 | #ifndef ARRAY_SIZE |
| 142 | #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) |
| 143 | #endif |
| 144 | #define HR_TO_S(x) ((x) * 60 * 60) |
| 145 | |
| 146 | /** |
| 147 | * Report the age of segments in the compressor. |
| 148 | */ |
| 149 | static void |
| 150 | report_compressor_age() |
| 151 | { |
| 152 | /* If the compressor is not configured, do nothing and return early. */ |
| 153 | if (vm_compressor_mode == VM_PAGER_NOT_CONFIGURED) { |
| 154 | os_log(OS_LOG_DEFAULT, "%s: vm_compressor_mode == VM_PAGER_NOT_CONFIGURED, returning early" , __func__); |
| 155 | return; |
| 156 | } |
| 157 | |
| 158 | const queue_head_t *c_queues[] = {&c_age_list_head, &c_major_list_head}; |
| 159 | c_reporting_bucket_t c_buckets[] = { |
| 160 | {.count = 0, .lower = HR_TO_S(0), .upper = HR_TO_S(1)}, /* [0, 1) hours */ |
| 161 | {.count = 0, .lower = HR_TO_S(1), .upper = HR_TO_S(6)}, /* [1, 6) hours */ |
| 162 | {.count = 0, .lower = HR_TO_S(6), .upper = HR_TO_S(12)}, /* [6, 12) hours */ |
| 163 | {.count = 0, .lower = HR_TO_S(12), .upper = HR_TO_S(24)}, /* [12, 24) hours */ |
| 164 | {.count = 0, .lower = HR_TO_S(24), .upper = HR_TO_S(36)}, /* [24, 36) hours */ |
| 165 | {.count = 0, .lower = HR_TO_S(36), .upper = HR_TO_S(48)}, /* [36, 48) hours */ |
| 166 | {.count = 0, .lower = HR_TO_S(48), .upper = C_REPORTING_BUCKETS_MAX}, /* [48, MAX) hours */ |
| 167 | }; |
| 168 | clock_sec_t now; |
| 169 | clock_nsec_t nsec; |
| 170 | |
| 171 | /* Collect the segments and update the bucket counts. */ |
| 172 | lck_mtx_lock_spin_always(c_list_lock); |
| 173 | for (unsigned q = 0; q < ARRAY_SIZE(c_queues); q++) { |
| 174 | c_segment_t c_seg = (c_segment_t) queue_first(c_queues[q]); |
| 175 | while (!queue_end(c_queues[q], (queue_entry_t) c_seg)) { |
| 176 | for (unsigned b = 0; b < ARRAY_SIZE(c_buckets); b++) { |
| 177 | uint32_t creation_ts = c_seg->c_creation_ts; |
| 178 | clock_get_system_nanotime(&now, &nsec); |
| 179 | clock_sec_t age = now - creation_ts; |
| 180 | if ((age >= c_buckets[b].lower) && |
| 181 | (age < c_buckets[b].upper)) { |
| 182 | c_buckets[b].count++; |
| 183 | break; |
| 184 | } |
| 185 | } |
| 186 | c_seg = (c_segment_t) queue_next(&c_seg->c_age_list); |
| 187 | } |
| 188 | } |
| 189 | lck_mtx_unlock_always(c_list_lock); |
| 190 | |
| 191 | /* Send the ages to CoreAnalytics. */ |
| 192 | ca_event_t event = CA_EVENT_ALLOCATE(compressor_age); |
| 193 | CA_EVENT_TYPE(compressor_age) * e = event->data; |
| 194 | e->hour1 = c_buckets[0].count; |
| 195 | e->hour6 = c_buckets[1].count; |
| 196 | e->hour12 = c_buckets[2].count; |
| 197 | e->hour24 = c_buckets[3].count; |
| 198 | e->hour36 = c_buckets[4].count; |
| 199 | e->hour48 = c_buckets[5].count; |
| 200 | e->hourMax = c_buckets[6].count; |
| 201 | add_trial_uuids(e->trial_treatment_id, e->trial_experiment_id); |
| 202 | e->trial_deployment_id = trial_deployment_id; |
| 203 | CA_EVENT_SEND(event); |
| 204 | } |
| 205 | #endif /* XNU_TARGET_OS_WATCH */ |
| 206 | |
| 207 | |
| 208 | extern uint64_t max_mem; |
| 209 | CA_EVENT(accounting_health, CA_INT, percentage); |
| 210 | /** |
| 211 | * Report health of resident vm page accounting. |
| 212 | */ |
| 213 | static void |
| 214 | report_accounting_health() |
| 215 | { |
| 216 | /** |
| 217 | * @note If a new accounting bucket is added, it must also be added in |
| 218 | * MemoryMaintenance sysstatuscheck, which panics when accounting reaches |
| 219 | * unhealthy levels. |
| 220 | */ |
| 221 | int64_t pages = (vm_page_wire_count |
| 222 | + vm_page_free_count |
| 223 | + vm_page_inactive_count |
| 224 | + vm_page_active_count |
| 225 | + VM_PAGE_COMPRESSOR_COUNT |
| 226 | + vm_page_speculative_count |
| 227 | #if CONFIG_SECLUDED_MEMORY |
| 228 | + vm_page_secluded_count |
| 229 | #endif /* CONFIG_SECLUDED_MEMORY */ |
| 230 | ); |
| 231 | int64_t percentage = (pages * 100) / (max_mem >> PAGE_SHIFT); |
| 232 | |
| 233 | /* Send the percentage health to CoreAnalytics. */ |
| 234 | ca_event_t event = CA_EVENT_ALLOCATE(accounting_health); |
| 235 | CA_EVENT_TYPE(accounting_health) * e = event->data; |
| 236 | e->percentage = percentage; |
| 237 | CA_EVENT_SEND(event); |
| 238 | } |
| 239 | |
| 240 | static void |
| 241 | schedule_analytics_thread_call() |
| 242 | { |
| 243 | static const uint64_t analytics_period_ns = ANALYTICS_PERIOD_HOURS * 60 * 60 * NSEC_PER_SEC; |
| 244 | uint64_t analytics_period_absolutetime; |
| 245 | nanoseconds_to_absolutetime(nanoseconds: analytics_period_ns, result: &analytics_period_absolutetime); |
| 246 | |
| 247 | thread_call_enter_delayed(call: vm_analytics_thread_call, deadline: analytics_period_absolutetime + mach_absolute_time()); |
| 248 | } |
| 249 | |
| 250 | /* |
| 251 | * This is the main entry point for reporting periodic analytics. |
| 252 | * It's called once every ANALYTICS_PERIOD_HOURS hours. |
| 253 | */ |
| 254 | void |
| 255 | vm_analytics_tick(void *arg0, void *arg1) |
| 256 | { |
| 257 | #pragma unused(arg0, arg1) |
| 258 | report_vm_swapusage(); |
| 259 | report_mlock_failures(); |
| 260 | #if XNU_TARGET_OS_WATCH |
| 261 | report_compressor_age(); |
| 262 | #endif /* XNU_TARGET_OS_WATCH */ |
| 263 | report_accounting_health(); |
| 264 | #if CONFIG_EXCLAVES |
| 265 | exclaves_memory_report_accounting(); |
| 266 | #endif /* CONFIG_EXCLAVES */ |
| 267 | schedule_analytics_thread_call(); |
| 268 | } |
| 269 | |
| 270 | static void |
| 271 | vm_analytics_init() |
| 272 | { |
| 273 | vm_analytics_thread_call = thread_call_allocate_with_options(func: vm_analytics_tick, NULL, pri: THREAD_CALL_PRIORITY_KERNEL, options: THREAD_CALL_OPTIONS_ONCE); |
| 274 | schedule_analytics_thread_call(); |
| 275 | } |
| 276 | |
| 277 | STARTUP(THREAD_CALL, STARTUP_RANK_MIDDLE, vm_analytics_init); |
| 278 | |