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