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 | |