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 "TelemetryIOInterposeObserver.h"
8 #include "core/TelemetryCommon.h"
9 #include "js/Array.h" // JS::NewArrayObject
10 #include "js/PropertyAndElement.h" // JS_DefineUCProperty
11 #include "js/ValueArray.h"
12 #include "nsIFile.h"
13
14 namespace mozilla::Telemetry {
15
TelemetryIOInterposeObserver(nsIFile * aXreDir)16 TelemetryIOInterposeObserver::TelemetryIOInterposeObserver(nsIFile* aXreDir)
17 : mCurStage(STAGE_STARTUP) {
18 nsAutoString xreDirPath;
19 nsresult rv = aXreDir->GetPath(xreDirPath);
20 if (NS_SUCCEEDED(rv)) {
21 AddPath(xreDirPath, u"{xre}"_ns);
22 }
23 }
24
AddPath(const nsAString & aPath,const nsAString & aSubstName)25 void TelemetryIOInterposeObserver::AddPath(const nsAString& aPath,
26 const nsAString& aSubstName) {
27 mSafeDirs.AppendElement(SafeDir(aPath, aSubstName));
28 }
29
30 // Threshold for reporting slow main-thread I/O (50 milliseconds).
31 const TimeDuration kTelemetryReportThreshold =
32 TimeDuration::FromMilliseconds(50);
33
Observe(Observation & aOb)34 void TelemetryIOInterposeObserver::Observe(Observation& aOb) {
35 // We only report main-thread I/O
36 if (!IsMainThread()) {
37 return;
38 }
39
40 if (aOb.ObservedOperation() == OpNextStage) {
41 mCurStage = NextStage(mCurStage);
42 MOZ_ASSERT(mCurStage < NUM_STAGES);
43 return;
44 }
45
46 if (aOb.Duration() < kTelemetryReportThreshold) {
47 return;
48 }
49
50 // Get the filename
51 nsAutoString filename;
52 aOb.Filename(filename);
53
54 // Discard observations without filename
55 if (filename.IsEmpty()) {
56 return;
57 }
58
59 #if defined(XP_WIN)
60 auto comparator = nsCaseInsensitiveStringComparator;
61 #else
62 auto comparator = nsTDefaultStringComparator<char16_t>;
63 #endif
64 nsAutoString processedName;
65 uint32_t safeDirsLen = mSafeDirs.Length();
66 for (uint32_t i = 0; i < safeDirsLen; ++i) {
67 if (StringBeginsWith(filename, mSafeDirs[i].mPath, comparator)) {
68 processedName = mSafeDirs[i].mSubstName;
69 processedName += Substring(filename, mSafeDirs[i].mPath.Length());
70 break;
71 }
72 }
73
74 if (processedName.IsEmpty()) {
75 return;
76 }
77
78 // Create a new entry or retrieve the existing one
79 FileIOEntryType* entry = mFileStats.PutEntry(processedName);
80 if (entry) {
81 FileStats& stats = entry->GetModifiableData()->mStats[mCurStage];
82 // Update the statistics
83 stats.totalTime += (double)aOb.Duration().ToMilliseconds();
84 switch (aOb.ObservedOperation()) {
85 case OpCreateOrOpen:
86 stats.creates++;
87 break;
88 case OpRead:
89 stats.reads++;
90 break;
91 case OpWrite:
92 stats.writes++;
93 break;
94 case OpFSync:
95 stats.fsyncs++;
96 break;
97 case OpStat:
98 stats.stats++;
99 break;
100 default:
101 break;
102 }
103 }
104 }
105
ReflectFileStats(FileIOEntryType * entry,JSContext * cx,JS::Handle<JSObject * > obj)106 bool TelemetryIOInterposeObserver::ReflectFileStats(FileIOEntryType* entry,
107 JSContext* cx,
108 JS::Handle<JSObject*> obj) {
109 JS::RootedValueArray<NUM_STAGES> stages(cx);
110
111 FileStatsByStage& statsByStage = *entry->GetModifiableData();
112 for (int s = STAGE_STARTUP; s < NUM_STAGES; ++s) {
113 FileStats& fileStats = statsByStage.mStats[s];
114
115 if (fileStats.totalTime == 0 && fileStats.creates == 0 &&
116 fileStats.reads == 0 && fileStats.writes == 0 &&
117 fileStats.fsyncs == 0 && fileStats.stats == 0) {
118 // Don't add an array that contains no information
119 stages[s].setNull();
120 continue;
121 }
122
123 // Array we want to report
124 JS::RootedValueArray<6> stats(cx);
125 stats[0].setNumber(fileStats.totalTime);
126 stats[1].setNumber(fileStats.creates);
127 stats[2].setNumber(fileStats.reads);
128 stats[3].setNumber(fileStats.writes);
129 stats[4].setNumber(fileStats.fsyncs);
130 stats[5].setNumber(fileStats.stats);
131
132 // Create jsStats as array of elements above
133 JS::RootedObject jsStats(cx, JS::NewArrayObject(cx, stats));
134 if (!jsStats) {
135 continue;
136 }
137
138 stages[s].setObject(*jsStats);
139 }
140
141 JS::Rooted<JSObject*> jsEntry(cx, JS::NewArrayObject(cx, stages));
142 if (!jsEntry) {
143 return false;
144 }
145
146 // Add jsEntry to top-level dictionary
147 const nsAString& key = entry->GetKey();
148 return JS_DefineUCProperty(cx, obj, key.Data(), key.Length(), jsEntry,
149 JSPROP_ENUMERATE | JSPROP_READONLY);
150 }
151
ReflectIntoJS(JSContext * cx,JS::Handle<JSObject * > rootObj)152 bool TelemetryIOInterposeObserver::ReflectIntoJS(
153 JSContext* cx, JS::Handle<JSObject*> rootObj) {
154 return mFileStats.ReflectIntoJS(ReflectFileStats, cx, rootObj);
155 }
156
157 /**
158 * Get size of hash table with file stats
159 */
160
SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const161 size_t TelemetryIOInterposeObserver::SizeOfIncludingThis(
162 mozilla::MallocSizeOf aMallocSizeOf) const {
163 return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
164 }
165
SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const166 size_t TelemetryIOInterposeObserver::SizeOfExcludingThis(
167 mozilla::MallocSizeOf aMallocSizeOf) const {
168 size_t size = 0;
169 size += mFileStats.ShallowSizeOfExcludingThis(aMallocSizeOf);
170 for (auto iter = mFileStats.ConstIter(); !iter.Done(); iter.Next()) {
171 size += iter.Get()->GetKey().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
172 }
173 size += mSafeDirs.ShallowSizeOfExcludingThis(aMallocSizeOf);
174 uint32_t safeDirsLen = mSafeDirs.Length();
175 for (uint32_t i = 0; i < safeDirsLen; ++i) {
176 size += mSafeDirs[i].SizeOfExcludingThis(aMallocSizeOf);
177 }
178 return size;
179 }
180
181 } // namespace mozilla::Telemetry
182