1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "MemoryTelemetry.h"
8 #include "nsMemoryReporterManager.h"
9
10 #include "GCTelemetry.h"
11 #include "mozilla/ClearOnShutdown.h"
12 #include "mozilla/Result.h"
13 #include "mozilla/ResultExtensions.h"
14 #include "mozilla/Services.h"
15 #include "mozilla/ScopeExit.h"
16 #include "mozilla/SimpleEnumerator.h"
17 #include "mozilla/Telemetry.h"
18 #include "mozilla/TimeStamp.h"
19 #include "mozilla/dom/ContentParent.h"
20 #include "mozilla/dom/ScriptSettings.h"
21 #include "nsContentUtils.h"
22 #include "nsIBrowserDOMWindow.h"
23 #include "nsIDOMChromeWindow.h"
24 #include "nsIMemoryReporter.h"
25 #include "nsIWindowMediator.h"
26 #include "nsImportModule.h"
27 #include "nsNetCID.h"
28 #include "nsObserverService.h"
29 #include "nsReadableUtils.h"
30 #include "nsThreadUtils.h"
31 #include "nsXULAppAPI.h"
32 #include "xpcpublic.h"
33
34 #include <cstdlib>
35
36 using namespace mozilla;
37
38 using mozilla::dom::AutoJSAPI;
39 using mozilla::dom::ContentParent;
40
41 // Do not gather data more than once a minute (ms)
42 static constexpr uint32_t kTelemetryInterval = 60 * 1000;
43
44 static constexpr const char* kTopicCycleCollectorBegin =
45 "cycle-collector-begin";
46
47 // How long to wait in millis for all the child memory reports to come in
48 static constexpr uint32_t kTotalMemoryCollectorTimeout = 200;
49
GetGCTelemetry()50 static Result<nsCOMPtr<mozIGCTelemetry>, nsresult> GetGCTelemetry() {
51 nsresult rv;
52
53 nsCOMPtr<mozIGCTelemetryJSM> jsm =
54 do_ImportModule("resource://gre/modules/GCTelemetry.jsm", &rv);
55 MOZ_TRY(rv);
56
57 nsCOMPtr<mozIGCTelemetry> gcTelemetry;
58 MOZ_TRY(jsm->GetGCTelemetry(getter_AddRefs(gcTelemetry)));
59
60 return std::move(gcTelemetry);
61 }
62
63 namespace {
64
65 enum class PrevValue : uint32_t {
66 #ifdef XP_WIN
67 LOW_MEMORY_EVENTS_VIRTUAL,
68 LOW_MEMORY_EVENTS_COMMIT_SPACE,
69 LOW_MEMORY_EVENTS_PHYSICAL,
70 #endif
71 #if defined(XP_LINUX) && !defined(ANDROID)
72 PAGE_FAULTS_HARD,
73 #endif
74 SIZE_,
75 };
76
77 } // anonymous namespace
78
79 constexpr uint32_t kUninitialized = ~0;
80
81 static uint32_t gPrevValues[uint32_t(PrevValue::SIZE_)];
82
PrevValueIndex(Telemetry::HistogramID aId)83 static uint32_t PrevValueIndex(Telemetry::HistogramID aId) {
84 switch (aId) {
85 #ifdef XP_WIN
86 case Telemetry::LOW_MEMORY_EVENTS_VIRTUAL:
87 return uint32_t(PrevValue::LOW_MEMORY_EVENTS_VIRTUAL);
88 case Telemetry::LOW_MEMORY_EVENTS_COMMIT_SPACE:
89 return uint32_t(PrevValue::LOW_MEMORY_EVENTS_COMMIT_SPACE);
90 case Telemetry::LOW_MEMORY_EVENTS_PHYSICAL:
91 return uint32_t(PrevValue::LOW_MEMORY_EVENTS_PHYSICAL);
92 #endif
93 #if defined(XP_LINUX) && !defined(ANDROID)
94 case Telemetry::PAGE_FAULTS_HARD:
95 return uint32_t(PrevValue::PAGE_FAULTS_HARD);
96 #endif
97 default:
98 MOZ_ASSERT_UNREACHABLE("Unexpected histogram ID");
99 return 0;
100 }
101 }
102
NS_IMPL_ISUPPORTS(MemoryTelemetry,nsIObserver,nsISupportsWeakReference)103 NS_IMPL_ISUPPORTS(MemoryTelemetry, nsIObserver, nsISupportsWeakReference)
104
105 MemoryTelemetry::MemoryTelemetry()
106 : mThreadPool(do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID)) {}
107
Init()108 void MemoryTelemetry::Init() {
109 for (auto& val : gPrevValues) {
110 val = kUninitialized;
111 }
112
113 if (XRE_IsContentProcess()) {
114 nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
115 MOZ_RELEASE_ASSERT(obs);
116
117 obs->AddObserver(this, "content-child-shutdown", true);
118 }
119 }
120
Get()121 /* static */ MemoryTelemetry& MemoryTelemetry::Get() {
122 static RefPtr<MemoryTelemetry> sInstance;
123
124 MOZ_ASSERT(NS_IsMainThread());
125
126 if (!sInstance) {
127 sInstance = new MemoryTelemetry();
128 sInstance->Init();
129 ClearOnShutdown(&sInstance);
130 }
131 return *sInstance;
132 }
133
DelayedInit()134 nsresult MemoryTelemetry::DelayedInit() {
135 if (Telemetry::CanRecordExtended()) {
136 nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
137 MOZ_RELEASE_ASSERT(obs);
138
139 obs->AddObserver(this, kTopicCycleCollectorBegin, true);
140 }
141
142 GatherReports();
143
144 if (Telemetry::CanRecordExtended()) {
145 nsCOMPtr<mozIGCTelemetry> gcTelemetry;
146 MOZ_TRY_VAR(gcTelemetry, GetGCTelemetry());
147
148 MOZ_TRY(gcTelemetry->Init());
149 }
150
151 return NS_OK;
152 }
153
Shutdown()154 nsresult MemoryTelemetry::Shutdown() {
155 nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
156 MOZ_RELEASE_ASSERT(obs);
157
158 obs->RemoveObserver(this, kTopicCycleCollectorBegin);
159
160 if (Telemetry::CanRecordExtended()) {
161 nsCOMPtr<mozIGCTelemetry> gcTelemetry;
162 MOZ_TRY_VAR(gcTelemetry, GetGCTelemetry());
163
164 MOZ_TRY(gcTelemetry->Shutdown());
165 }
166
167 return NS_OK;
168 }
169
HandleMemoryReport(Telemetry::HistogramID aId,int32_t aUnits,uint64_t aAmount,const nsCString & aKey=VoidCString ())170 static inline void HandleMemoryReport(Telemetry::HistogramID aId,
171 int32_t aUnits, uint64_t aAmount,
172 const nsCString& aKey = VoidCString()) {
173 uint32_t val;
174 switch (aUnits) {
175 case nsIMemoryReporter::UNITS_BYTES:
176 val = uint32_t(aAmount / 1024);
177 break;
178
179 case nsIMemoryReporter::UNITS_PERCENTAGE:
180 // UNITS_PERCENTAGE amounts are 100x greater than their raw value.
181 val = uint32_t(aAmount / 100);
182 break;
183
184 case nsIMemoryReporter::UNITS_COUNT:
185 val = uint32_t(aAmount);
186 break;
187
188 case nsIMemoryReporter::UNITS_COUNT_CUMULATIVE: {
189 // If the reporter gives us a cumulative count, we'll report the
190 // difference in its value between now and our previous ping.
191
192 uint32_t idx = PrevValueIndex(aId);
193 uint32_t prev = gPrevValues[idx];
194 gPrevValues[idx] = aAmount;
195
196 if (prev == kUninitialized) {
197 // If this is the first time we're reading this reporter, store its
198 // current value but don't report it in the telemetry ping, so we
199 // ignore the effect startup had on the reporter.
200 return;
201 }
202 val = aAmount - prev;
203 break;
204 }
205
206 default:
207 MOZ_ASSERT_UNREACHABLE("Unexpected aUnits value");
208 return;
209 }
210
211 // Note: The reference equality check here should allow the compiler to
212 // optimize this case out at compile time when we weren't given a key,
213 // while IsEmpty() or IsVoid() most likely will not.
214 if (&aKey == &VoidCString()) {
215 Telemetry::Accumulate(aId, val);
216 } else {
217 Telemetry::Accumulate(aId, aKey, val);
218 }
219 }
220
GatherReports(const std::function<void ()> & aCompletionCallback)221 nsresult MemoryTelemetry::GatherReports(
222 const std::function<void()>& aCompletionCallback) {
223 auto cleanup = MakeScopeExit([&]() {
224 if (aCompletionCallback) {
225 aCompletionCallback();
226 }
227 });
228
229 RefPtr<nsMemoryReporterManager> mgr = nsMemoryReporterManager::GetOrCreate();
230 MOZ_DIAGNOSTIC_ASSERT(mgr);
231 NS_ENSURE_TRUE(mgr, NS_ERROR_FAILURE);
232
233 #define RECORD(id, metric, units) \
234 do { \
235 int64_t amt; \
236 nsresult rv = mgr->Get##metric(&amt); \
237 if (NS_SUCCEEDED(rv)) { \
238 HandleMemoryReport(Telemetry::id, nsIMemoryReporter::units, amt); \
239 } else if (rv != NS_ERROR_NOT_AVAILABLE) { \
240 NS_WARNING("Failed to retrieve memory telemetry for " #metric); \
241 } \
242 } while (0)
243
244 // GHOST_WINDOWS is opt-out as of Firefox 55
245 RECORD(GHOST_WINDOWS, GhostWindows, UNITS_COUNT);
246
247 // If we're running in the parent process, collect data from all processes for
248 // the MEMORY_TOTAL histogram.
249 if (XRE_IsParentProcess() && !mTotalMemoryGatherer) {
250 mTotalMemoryGatherer = new TotalMemoryGatherer();
251 mTotalMemoryGatherer->Begin(mThreadPool);
252 }
253
254 if (!Telemetry::CanRecordReleaseData()) {
255 return NS_OK;
256 }
257
258 // Get memory measurements from distinguished amount attributes. We used
259 // to measure "explicit" too, but it could cause hangs, and the data was
260 // always really noisy anyway. See bug 859657.
261 //
262 // test_TelemetrySession.js relies on some of these histograms being
263 // here. If you remove any of the following histograms from here, you'll
264 // have to modify test_TelemetrySession.js:
265 //
266 // * MEMORY_TOTAL,
267 // * MEMORY_JS_GC_HEAP, and
268 // * MEMORY_JS_COMPARTMENTS_SYSTEM.
269 //
270 // The distinguished amount attribute names don't match the telemetry id
271 // names in some cases due to a combination of (a) historical reasons, and
272 // (b) the fact that we can't change telemetry id names without breaking
273 // data continuity.
274
275 // Collect cheap or main-thread only metrics synchronously, on the main
276 // thread.
277 RECORD(MEMORY_JS_GC_HEAP, JSMainRuntimeGCHeap, UNITS_BYTES);
278 RECORD(MEMORY_JS_COMPARTMENTS_SYSTEM, JSMainRuntimeCompartmentsSystem,
279 UNITS_COUNT);
280 RECORD(MEMORY_JS_COMPARTMENTS_USER, JSMainRuntimeCompartmentsUser,
281 UNITS_COUNT);
282 RECORD(MEMORY_JS_REALMS_SYSTEM, JSMainRuntimeRealmsSystem, UNITS_COUNT);
283 RECORD(MEMORY_JS_REALMS_USER, JSMainRuntimeRealmsUser, UNITS_COUNT);
284 RECORD(MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED, ImagesContentUsedUncompressed,
285 UNITS_BYTES);
286 RECORD(MEMORY_STORAGE_SQLITE, StorageSQLite, UNITS_BYTES);
287 #ifdef XP_WIN
288 RECORD(LOW_MEMORY_EVENTS_VIRTUAL, LowMemoryEventsVirtual,
289 UNITS_COUNT_CUMULATIVE);
290 RECORD(LOW_MEMORY_EVENTS_COMMIT_SPACE, LowMemoryEventsCommitSpace,
291 UNITS_COUNT_CUMULATIVE);
292 RECORD(LOW_MEMORY_EVENTS_PHYSICAL, LowMemoryEventsPhysical,
293 UNITS_COUNT_CUMULATIVE);
294 #endif
295 #if defined(XP_LINUX) && !defined(ANDROID)
296 RECORD(PAGE_FAULTS_HARD, PageFaultsHard, UNITS_COUNT_CUMULATIVE);
297 #endif
298
299 RefPtr<Runnable> completionRunnable;
300 if (aCompletionCallback) {
301 completionRunnable = NS_NewRunnableFunction(__func__, aCompletionCallback);
302 }
303
304 // Collect expensive metrics that can be calculated off-main-thread
305 // asynchronously, on a background thread.
306 RefPtr<Runnable> runnable = NS_NewRunnableFunction(
307 "MemoryTelemetry::GatherReports", [mgr, completionRunnable]() mutable {
308 RECORD(MEMORY_VSIZE, Vsize, UNITS_BYTES);
309 #if !defined(HAVE_64BIT_BUILD) || !defined(XP_WIN)
310 RECORD(MEMORY_VSIZE_MAX_CONTIGUOUS, VsizeMaxContiguous, UNITS_BYTES);
311 #endif
312 RECORD(MEMORY_RESIDENT_FAST, ResidentFast, UNITS_BYTES);
313 RECORD(MEMORY_RESIDENT_PEAK, ResidentPeak, UNITS_BYTES);
314 RECORD(MEMORY_UNIQUE, ResidentUnique, UNITS_BYTES);
315 RECORD(MEMORY_HEAP_ALLOCATED, HeapAllocated, UNITS_BYTES);
316 RECORD(MEMORY_HEAP_OVERHEAD_FRACTION, HeapOverheadFraction,
317 UNITS_PERCENTAGE);
318
319 if (completionRunnable) {
320 NS_DispatchToMainThread(completionRunnable.forget(),
321 NS_DISPATCH_NORMAL);
322 }
323 });
324
325 #undef RECORD
326
327 nsresult rv = mThreadPool->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
328 if (!NS_WARN_IF(NS_FAILED(rv))) {
329 cleanup.release();
330 }
331
332 return NS_OK;
333 }
334
NS_IMPL_ISUPPORTS(MemoryTelemetry::TotalMemoryGatherer,nsITimerCallback)335 NS_IMPL_ISUPPORTS(MemoryTelemetry::TotalMemoryGatherer, nsITimerCallback)
336
337 /**
338 * Polls all child processes for their unique set size, and populates the
339 * MEMORY_TOTAL and MEMORY_DISTRIBUTION_AMONG_CONTENT histograms with the
340 * results.
341 */
342 void MemoryTelemetry::TotalMemoryGatherer::Begin(nsIEventTarget* aThreadPool) {
343 nsCOMPtr<nsISerialEventTarget> target = GetMainThreadSerialEventTarget();
344
345 nsTArray<ContentParent*> parents;
346 ContentParent::GetAll(parents);
347 for (auto& parent : parents) {
348 mRemainingChildCount++;
349 parent->SendGetMemoryUniqueSetSize()->Then(
350 target, "TotalMemoryGather::Begin", this,
351 &TotalMemoryGatherer::CollectResult, &TotalMemoryGatherer::OnFailure);
352 }
353
354 mChildSizes.SetCapacity(mRemainingChildCount);
355
356 RefPtr<TotalMemoryGatherer> self{this};
357
358 aThreadPool->Dispatch(
359 NS_NewRunnableFunction(
360 "TotalMemoryGather::Begin",
361 [self]() {
362 RefPtr<nsMemoryReporterManager> mgr =
363 nsMemoryReporterManager::GetOrCreate();
364 MOZ_RELEASE_ASSERT(mgr);
365
366 NS_DispatchToMainThread(NewRunnableMethod<int64_t>(
367 "TotalMemoryGather::CollectParentSize", self,
368 &TotalMemoryGatherer::CollectParentSize, mgr->ResidentFast()));
369 }),
370 NS_DISPATCH_NORMAL);
371
372 NS_NewTimerWithCallback(getter_AddRefs(mTimeout), this,
373 kTotalMemoryCollectorTimeout,
374 nsITimer::TYPE_ONE_SHOT);
375 }
376
MaybeFinish()377 nsresult MemoryTelemetry::TotalMemoryGatherer::MaybeFinish() {
378 // If we timed out waiting for a response from any child, we don't report
379 // anything for this attempt.
380 if (!mTimeout || !mHaveParentSize || mRemainingChildCount) {
381 return NS_OK;
382 }
383
384 mTimeout = nullptr;
385 MemoryTelemetry::Get().mTotalMemoryGatherer = nullptr;
386
387 HandleMemoryReport(Telemetry::MEMORY_TOTAL, nsIMemoryReporter::UNITS_BYTES,
388 mTotalMemory);
389
390 if (mChildSizes.Length() > 1) {
391 int32_t tabsCount;
392 MOZ_TRY_VAR(tabsCount, GetOpenTabsCount());
393
394 nsCString key;
395 if (tabsCount <= 10) {
396 key = "0 - 10 tabs";
397 } else if (tabsCount <= 500) {
398 key = "11 - 500 tabs";
399 } else {
400 key = "more tabs";
401 }
402
403 // Mean of the USS of all the content processes.
404 int64_t mean = 0;
405 for (auto size : mChildSizes) {
406 mean += size;
407 }
408 mean /= mChildSizes.Length();
409
410 // For some users, for unknown reasons (though most likely because they're
411 // in a sandbox without procfs mounted), we wind up with 0 here, which
412 // triggers a floating point exception if we try to calculate values using
413 // it.
414 if (!mean) {
415 return NS_ERROR_UNEXPECTED;
416 }
417
418 // Absolute error of USS for each content process, normalized by the mean
419 // (*100 to get it in percentage). 20% means for a content process that it
420 // is using 20% more or 20% less than the mean.
421 for (auto size : mChildSizes) {
422 int64_t diff = llabs(size - mean) * 100 / mean;
423
424 HandleMemoryReport(Telemetry::MEMORY_DISTRIBUTION_AMONG_CONTENT,
425 nsIMemoryReporter::UNITS_COUNT, diff, key);
426 }
427 }
428
429 // This notification is for testing only.
430 if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
431 obs->NotifyObservers(nullptr, "gather-memory-telemetry-finished", nullptr);
432 }
433
434 return NS_OK;
435 }
436
CollectParentSize(int64_t aResident)437 void MemoryTelemetry::TotalMemoryGatherer::CollectParentSize(
438 int64_t aResident) {
439 mTotalMemory += aResident;
440 mHaveParentSize = true;
441
442 MaybeFinish();
443 }
444
CollectResult(int64_t aChildUSS)445 void MemoryTelemetry::TotalMemoryGatherer::CollectResult(int64_t aChildUSS) {
446 mChildSizes.AppendElement(aChildUSS);
447
448 mTotalMemory += aChildUSS;
449 mRemainingChildCount--;
450
451 MaybeFinish();
452 }
453
OnFailure(mozilla::ipc::ResponseRejectReason aReason)454 void MemoryTelemetry::TotalMemoryGatherer::OnFailure(
455 mozilla::ipc::ResponseRejectReason aReason) {
456 // Treat failure of any request the same as a timeout.
457 Notify(nullptr);
458 }
459
Notify(nsITimer * aTimer)460 nsresult MemoryTelemetry::TotalMemoryGatherer::Notify(nsITimer* aTimer) {
461 // Set mTimeout null to indicate the timeout has fired. After this, all
462 // results for this attempt will be ignored.
463 mTimeout = nullptr;
464 MemoryTelemetry::Get().mTotalMemoryGatherer = nullptr;
465 return NS_OK;
466 }
467
GetOpenTabsCount()468 /* static */ Result<uint32_t, nsresult> MemoryTelemetry::GetOpenTabsCount() {
469 nsresult rv;
470
471 nsCOMPtr<nsIWindowMediator> windowMediator(
472 do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv));
473 MOZ_TRY(rv);
474
475 nsCOMPtr<nsISimpleEnumerator> enumerator;
476 MOZ_TRY(windowMediator->GetEnumerator(u"navigator:browser",
477 getter_AddRefs(enumerator)));
478
479 uint32_t total = 0;
480 for (auto& window : SimpleEnumerator<nsIDOMChromeWindow>(enumerator)) {
481 nsCOMPtr<nsIBrowserDOMWindow> browserWin;
482 MOZ_TRY(window->GetBrowserDOMWindow(getter_AddRefs(browserWin)));
483
484 NS_ENSURE_TRUE(browserWin, Err(NS_ERROR_UNEXPECTED));
485
486 uint32_t tabCount;
487 MOZ_TRY(browserWin->GetTabCount(&tabCount));
488 total += tabCount;
489 }
490
491 return total;
492 }
493
GetUniqueSetSize(std::function<void (const int64_t &)> && aCallback)494 void MemoryTelemetry::GetUniqueSetSize(
495 std::function<void(const int64_t&)>&& aCallback) {
496 mThreadPool->Dispatch(
497 NS_NewRunnableFunction(
498 "MemoryTelemetry::GetUniqueSetSize",
499 [callback = std::move(aCallback)]() mutable {
500 RefPtr<nsMemoryReporterManager> mgr =
501 nsMemoryReporterManager::GetOrCreate();
502 MOZ_RELEASE_ASSERT(mgr);
503
504 int64_t uss = mgr->ResidentUnique();
505
506 NS_DispatchToMainThread(NS_NewRunnableFunction(
507 "MemoryTelemetry::GetUniqueSetSizeResult",
508 [uss, callback = std::move(callback)]() { callback(uss); }));
509 }),
510 NS_DISPATCH_NORMAL);
511 }
512
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)513 nsresult MemoryTelemetry::Observe(nsISupports* aSubject, const char* aTopic,
514 const char16_t* aData) {
515 if (strcmp(aTopic, kTopicCycleCollectorBegin) == 0) {
516 auto now = TimeStamp::Now();
517 if (!mLastPoll.IsNull() &&
518 (now - mLastPoll).ToMilliseconds() < kTelemetryInterval) {
519 return NS_OK;
520 }
521
522 mLastPoll = now;
523
524 NS_DispatchToCurrentThreadQueue(
525 NewRunnableMethod<std::function<void()>>(
526 "MemoryTelemetry::GatherReports", this,
527 &MemoryTelemetry::GatherReports, nullptr),
528 EventQueuePriority::Idle);
529 } else if (strcmp(aTopic, "content-child-shutdown") == 0) {
530 if (nsCOMPtr<nsITelemetry> telemetry =
531 do_GetService("@mozilla.org/base/telemetry;1")) {
532 telemetry->FlushBatchedChildTelemetry();
533 }
534 }
535 return NS_OK;
536 }
537