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