/src/mozilla-central/toolkit/components/backgroundhangmonitor/ThreadStackHelper.cpp
Line | Count | Source (jump to first uncovered line) |
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 "ThreadStackHelper.h" |
8 | | #include "MainThreadUtils.h" |
9 | | #include "nsJSPrincipals.h" |
10 | | #include "nsScriptSecurityManager.h" |
11 | | #include "jsfriendapi.h" |
12 | | #ifdef MOZ_THREADSTACKHELPER_PROFILING_STACK |
13 | | #include "js/ProfilingStack.h" |
14 | | #endif |
15 | | |
16 | | #include "mozilla/Assertions.h" |
17 | | #include "mozilla/Attributes.h" |
18 | | #include "mozilla/IntegerPrintfMacros.h" |
19 | | #include "mozilla/Move.h" |
20 | | #include "mozilla/Scoped.h" |
21 | | #include "mozilla/UniquePtr.h" |
22 | | #include "mozilla/MemoryChecking.h" |
23 | | #include "mozilla/Sprintf.h" |
24 | | #include "nsThread.h" |
25 | | #include "mozilla/HangTypes.h" |
26 | | |
27 | | #ifdef __GNUC__ |
28 | | # pragma GCC diagnostic push |
29 | | # pragma GCC diagnostic ignored "-Wshadow" |
30 | | #endif |
31 | | |
32 | | #if defined(MOZ_VALGRIND) |
33 | | # include <valgrind/valgrind.h> |
34 | | #endif |
35 | | |
36 | | #include <string.h> |
37 | | #include <vector> |
38 | | #include <cstdlib> |
39 | | |
40 | | #ifdef XP_LINUX |
41 | | #include <ucontext.h> |
42 | | #include <unistd.h> |
43 | | #include <sys/syscall.h> |
44 | | #endif |
45 | | |
46 | | #ifdef __GNUC__ |
47 | | # pragma GCC diagnostic pop // -Wshadow |
48 | | #endif |
49 | | |
50 | | #if defined(XP_LINUX) || defined(XP_MACOSX) |
51 | | #include <pthread.h> |
52 | | #endif |
53 | | |
54 | | #ifdef ANDROID |
55 | | #ifndef SYS_gettid |
56 | | #define SYS_gettid __NR_gettid |
57 | | #endif |
58 | | #if defined(__arm__) && !defined(__NR_rt_tgsigqueueinfo) |
59 | | // Some NDKs don't define this constant even though the kernel supports it. |
60 | | #define __NR_rt_tgsigqueueinfo (__NR_SYSCALL_BASE+363) |
61 | | #endif |
62 | | #ifndef SYS_rt_tgsigqueueinfo |
63 | | #define SYS_rt_tgsigqueueinfo __NR_rt_tgsigqueueinfo |
64 | | #endif |
65 | | #endif |
66 | | |
67 | | namespace mozilla { |
68 | | |
69 | | ThreadStackHelper::ThreadStackHelper() |
70 | | : mStackToFill(nullptr) |
71 | | , mMaxStackSize(16) |
72 | | , mMaxBufferSize(512) |
73 | | , mDesiredStackSize(0) |
74 | | , mDesiredBufferSize(0) |
75 | 3 | { |
76 | 3 | mThreadId = profiler_current_thread_id(); |
77 | 3 | } |
78 | | |
79 | | bool |
80 | | ThreadStackHelper::PrepareStackBuffer(HangStack& aStack) |
81 | 0 | { |
82 | 0 | // If we need to grow because we used more than we could store last time, |
83 | 0 | // increase our maximum sizes for this time. |
84 | 0 | if (mDesiredBufferSize > mMaxBufferSize) { |
85 | 0 | mMaxBufferSize = mDesiredBufferSize; |
86 | 0 | } |
87 | 0 | if (mDesiredStackSize > mMaxStackSize) { |
88 | 0 | mMaxStackSize = mDesiredStackSize; |
89 | 0 | } |
90 | 0 | mDesiredBufferSize = 0; |
91 | 0 | mDesiredStackSize = 0; |
92 | 0 |
|
93 | 0 | // Clear all of the stack entries. |
94 | 0 | aStack.stack().ClearAndRetainStorage(); |
95 | 0 | aStack.strbuffer().ClearAndRetainStorage(); |
96 | 0 | aStack.modules().Clear(); |
97 | 0 |
|
98 | 0 | #ifdef MOZ_THREADSTACKHELPER_PROFILING_STACK |
99 | 0 | // Ensure we have enough space in our stack and string buffers for the data we |
100 | 0 | // want to collect. |
101 | 0 | if (!aStack.stack().SetCapacity(mMaxStackSize, fallible) || |
102 | 0 | !aStack.strbuffer().SetCapacity(mMaxBufferSize, fallible)) { |
103 | 0 | return false; |
104 | 0 | } |
105 | 0 | return true; |
106 | | #else |
107 | | return false; |
108 | | #endif |
109 | | } |
110 | | |
111 | | namespace { |
112 | | template<typename T> |
113 | | class ScopedSetPtr |
114 | | { |
115 | | private: |
116 | | T*& mPtr; |
117 | | public: |
118 | 0 | ScopedSetPtr(T*& p, T* val) : mPtr(p) { mPtr = val; } Unexecuted instantiation: Unified_cpp_ackgroundhangmonitor0.cpp:mozilla::(anonymous namespace)::ScopedSetPtr<mozilla::HangStack>::ScopedSetPtr(mozilla::HangStack*&, mozilla::HangStack*) Unexecuted instantiation: Unified_cpp_ackgroundhangmonitor0.cpp:mozilla::(anonymous namespace)::ScopedSetPtr<mozilla::Array<char, 1000ul> >::ScopedSetPtr(mozilla::Array<char, 1000ul>*&, mozilla::Array<char, 1000ul>*) |
119 | 0 | ~ScopedSetPtr() { mPtr = nullptr; } Unexecuted instantiation: Unified_cpp_ackgroundhangmonitor0.cpp:mozilla::(anonymous namespace)::ScopedSetPtr<mozilla::HangStack>::~ScopedSetPtr() Unexecuted instantiation: Unified_cpp_ackgroundhangmonitor0.cpp:mozilla::(anonymous namespace)::ScopedSetPtr<mozilla::Array<char, 1000ul> >::~ScopedSetPtr() |
120 | | }; |
121 | | } // namespace |
122 | | |
123 | | void |
124 | | ThreadStackHelper::GetStack(HangStack& aStack, nsACString& aRunnableName, bool aStackWalk) |
125 | 0 | { |
126 | 0 | aRunnableName.AssignLiteral("???"); |
127 | 0 |
|
128 | 0 | if (!PrepareStackBuffer(aStack)) { |
129 | 0 | return; |
130 | 0 | } |
131 | 0 | |
132 | 0 | Array<char, nsThread::kRunnableNameBufSize> runnableName; |
133 | 0 | runnableName[0] = '\0'; |
134 | 0 |
|
135 | 0 | ScopedSetPtr<HangStack> _stackGuard(mStackToFill, &aStack); |
136 | 0 | ScopedSetPtr<Array<char, nsThread::kRunnableNameBufSize>> |
137 | 0 | _runnableGuard(mRunnableNameBuffer, &runnableName); |
138 | 0 |
|
139 | 0 | // XXX: We don't need to pass in ProfilerFeature::StackWalk to trigger |
140 | 0 | // stackwalking, as that is instead controlled by the last argument. |
141 | 0 | profiler_suspend_and_sample_thread( |
142 | 0 | mThreadId, ProfilerFeature::Privacy, *this, aStackWalk); |
143 | 0 |
|
144 | 0 | // Copy the name buffer allocation into the output string. We explicitly set |
145 | 0 | // the last byte to null in case we read in some corrupted data without a null |
146 | 0 | // terminator. |
147 | 0 | runnableName[nsThread::kRunnableNameBufSize - 1] = '\0'; |
148 | 0 | aRunnableName.AssignASCII(runnableName.cbegin()); |
149 | 0 | } |
150 | | |
151 | | void |
152 | | ThreadStackHelper::SetIsMainThread() |
153 | 0 | { |
154 | 0 | MOZ_RELEASE_ASSERT(mRunnableNameBuffer); |
155 | 0 |
|
156 | 0 | // NOTE: We cannot allocate any memory in this callback, as the target |
157 | 0 | // thread is suspended, so we first copy it into a stack-allocated buffer, |
158 | 0 | // and then once the target thread is resumed, we can copy it into a real |
159 | 0 | // nsCString. |
160 | 0 | // |
161 | 0 | // Currently we only store the names of runnables which are running on the |
162 | 0 | // main thread, so we only want to read sMainThreadRunnableName and copy its |
163 | 0 | // value in the case that we are currently suspending the main thread. |
164 | 0 | *mRunnableNameBuffer = nsThread::sMainThreadRunnableName; |
165 | 0 | } |
166 | | |
167 | | void |
168 | | ThreadStackHelper::TryAppendFrame(HangEntry aFrame) |
169 | 0 | { |
170 | 0 | MOZ_RELEASE_ASSERT(mStackToFill); |
171 | 0 |
|
172 | 0 | // We deduplicate identical Content, Jit, Wasm, ChromeScript and Suppressed frames. |
173 | 0 | switch (aFrame.type()) { |
174 | 0 | case HangEntry::THangEntryContent: |
175 | 0 | case HangEntry::THangEntryJit: |
176 | 0 | case HangEntry::THangEntryWasm: |
177 | 0 | case HangEntry::THangEntryChromeScript: |
178 | 0 | case HangEntry::THangEntrySuppressed: |
179 | 0 | if (!mStackToFill->stack().IsEmpty() && |
180 | 0 | mStackToFill->stack().LastElement().type() == aFrame.type()) { |
181 | 0 | return; |
182 | 0 | } |
183 | 0 | break; |
184 | 0 | default: |
185 | 0 | break; |
186 | 0 | } |
187 | 0 | |
188 | 0 | // Record that we _want_ to use another frame entry. If this exceeds |
189 | 0 | // mMaxStackSize, we'll allocate more room on the next hang. |
190 | 0 | mDesiredStackSize += 1; |
191 | 0 |
|
192 | 0 | // Perform the append if we have enough space to do so. |
193 | 0 | if (mStackToFill->stack().Capacity() > mStackToFill->stack().Length()) { |
194 | 0 | mStackToFill->stack().AppendElement(std::move(aFrame)); |
195 | 0 | } |
196 | 0 | } |
197 | | |
198 | | void |
199 | | ThreadStackHelper::CollectNativeLeafAddr(void* aAddr) |
200 | 0 | { |
201 | 0 | MOZ_RELEASE_ASSERT(mStackToFill); |
202 | 0 | TryAppendFrame(HangEntryProgCounter(reinterpret_cast<uintptr_t>(aAddr))); |
203 | 0 | } |
204 | | |
205 | | void |
206 | | ThreadStackHelper::CollectJitReturnAddr(void* aAddr) |
207 | 0 | { |
208 | 0 | MOZ_RELEASE_ASSERT(mStackToFill); |
209 | 0 | TryAppendFrame(HangEntryJit()); |
210 | 0 | } |
211 | | |
212 | | void |
213 | | ThreadStackHelper::CollectWasmFrame(const char* aLabel) |
214 | 0 | { |
215 | 0 | MOZ_RELEASE_ASSERT(mStackToFill); |
216 | 0 | // We don't want to collect WASM frames, as they are probably for content, so |
217 | 0 | // we just add a "(content wasm)" frame. |
218 | 0 | TryAppendFrame(HangEntryWasm()); |
219 | 0 | } |
220 | | |
221 | | namespace { |
222 | | |
223 | | bool |
224 | | IsChromeJSScript(JSScript* aScript) |
225 | 0 | { |
226 | 0 | // May be called from another thread or inside a signal handler. |
227 | 0 | // We assume querying the script is safe but we must not manipulate it. |
228 | 0 | nsIScriptSecurityManager* const secman = |
229 | 0 | nsScriptSecurityManager::GetScriptSecurityManager(); |
230 | 0 | NS_ENSURE_TRUE(secman, false); |
231 | 0 |
|
232 | 0 | JSPrincipals* const principals = JS_GetScriptPrincipals(aScript); |
233 | 0 | return secman->IsSystemPrincipal(nsJSPrincipals::get(principals)); |
234 | 0 | } |
235 | | |
236 | | // Get the full path after the URI scheme, if the URI matches the scheme. |
237 | | // For example, GetFullPathForScheme("a://b/c/d/e", "a://") returns "b/c/d/e". |
238 | | template <size_t LEN> |
239 | | const char* |
240 | 0 | GetFullPathForScheme(const char* filename, const char (&scheme)[LEN]) { |
241 | 0 | // Account for the null terminator included in LEN. |
242 | 0 | if (!strncmp(filename, scheme, LEN - 1)) { |
243 | 0 | return filename + LEN - 1; |
244 | 0 | } |
245 | 0 | return nullptr; |
246 | 0 | } Unexecuted instantiation: Unified_cpp_ackgroundhangmonitor0.cpp:char const* mozilla::(anonymous namespace)::GetFullPathForScheme<10ul>(char const*, char const (&) [10ul]) Unexecuted instantiation: Unified_cpp_ackgroundhangmonitor0.cpp:char const* mozilla::(anonymous namespace)::GetFullPathForScheme<12ul>(char const*, char const (&) [12ul]) |
247 | | |
248 | | // Get the full path after a URI component, if the URI contains the component. |
249 | | // For example, GetPathAfterComponent("a://b/c/d/e", "/c/") returns "d/e". |
250 | | template <size_t LEN> |
251 | | const char* |
252 | 0 | GetPathAfterComponent(const char* filename, const char (&component)[LEN]) { |
253 | 0 | const char* found = nullptr; |
254 | 0 | const char* next = strstr(filename, component); |
255 | 0 | while (next) { |
256 | 0 | // Move 'found' to end of the component, after the separator '/'. |
257 | 0 | // 'LEN - 1' accounts for the null terminator included in LEN, |
258 | 0 | found = next + LEN - 1; |
259 | 0 | // Resume searching before the separator '/'. |
260 | 0 | next = strstr(found - 1, component); |
261 | 0 | } |
262 | 0 | return found; |
263 | 0 | } Unexecuted instantiation: Unified_cpp_ackgroundhangmonitor0.cpp:char const* mozilla::(anonymous namespace)::GetPathAfterComponent<5ul>(char const*, char const (&) [5ul]) Unexecuted instantiation: Unified_cpp_ackgroundhangmonitor0.cpp:char const* mozilla::(anonymous namespace)::GetPathAfterComponent<13ul>(char const*, char const (&) [13ul]) |
264 | | |
265 | | } // namespace |
266 | | |
267 | | void |
268 | | ThreadStackHelper::CollectProfilingStackFrame(const js::ProfilingStackFrame& aFrame) |
269 | 0 | { |
270 | 0 | // For non-js frames we just include the raw label. |
271 | 0 | if (!aFrame.isJsFrame()) { |
272 | 0 | const char* frameLabel = aFrame.label(); |
273 | 0 |
|
274 | 0 | // frameLabel is a statically allocated string, so we want to store a |
275 | 0 | // reference to it without performing any allocations. This is important, as |
276 | 0 | // we aren't allowed to allocate within this function. |
277 | 0 | // |
278 | 0 | // The variant for this kind of label in our HangStack object is a |
279 | 0 | // `nsCString`, which normally contains heap allocated string data. However, |
280 | 0 | // `nsCString` has an optimization for literal strings which causes the |
281 | 0 | // backing data to not be copied when being copied between nsCString |
282 | 0 | // objects. |
283 | 0 | // |
284 | 0 | // We take advantage of that optimization by creating a nsCString object |
285 | 0 | // which has the LITERAL flag set. Without this optimization, this code |
286 | 0 | // would be incorrect. |
287 | 0 | nsCString label; |
288 | 0 | label.AssignLiteral(frameLabel, strlen(frameLabel)); |
289 | 0 |
|
290 | 0 | // Let's make sure we don't deadlock here, by asserting that `label`'s |
291 | 0 | // backing data matches. |
292 | 0 | MOZ_RELEASE_ASSERT(label.BeginReading() == frameLabel, |
293 | 0 | "String copy performed during ThreadStackHelper::CollectProfilingStackFrame"); |
294 | 0 | TryAppendFrame(label); |
295 | 0 | return; |
296 | 0 | } |
297 | 0 | |
298 | 0 | if (!aFrame.script()) { |
299 | 0 | TryAppendFrame(HangEntrySuppressed()); |
300 | 0 | return; |
301 | 0 | } |
302 | 0 | |
303 | 0 | if (!IsChromeJSScript(aFrame.script())) { |
304 | 0 | TryAppendFrame(HangEntryContent()); |
305 | 0 | return; |
306 | 0 | } |
307 | 0 | |
308 | 0 | // Rather than using the profiler's dynamic string, we compute our own string. |
309 | 0 | // This is because we want to do some size-saving strategies, and throw out |
310 | 0 | // information which won't help us as much. |
311 | 0 | // XXX: We currently don't collect the function name which hung. |
312 | 0 | const char* filename = JS_GetScriptFilename(aFrame.script()); |
313 | 0 | unsigned lineno = JS_PCToLineNumber(aFrame.script(), aFrame.pc()); |
314 | 0 |
|
315 | 0 | // Some script names are in the form "foo -> bar -> baz". |
316 | 0 | // Here we find the origin of these redirected scripts. |
317 | 0 | const char* basename = GetPathAfterComponent(filename, " -> "); |
318 | 0 | if (basename) { |
319 | 0 | filename = basename; |
320 | 0 | } |
321 | 0 |
|
322 | 0 | // Strip chrome:// or resource:// off of the filename if present. |
323 | 0 | basename = GetFullPathForScheme(filename, "chrome://"); |
324 | 0 | if (!basename) { |
325 | 0 | basename = GetFullPathForScheme(filename, "resource://"); |
326 | 0 | } |
327 | 0 | if (!basename) { |
328 | 0 | // If we're in an add-on script, under the {profile}/extensions |
329 | 0 | // directory, extract the path after the /extensions/ part. |
330 | 0 | basename = GetPathAfterComponent(filename, "/extensions/"); |
331 | 0 | } |
332 | 0 | if (!basename) { |
333 | 0 | // Only keep the file base name for paths outside the above formats. |
334 | 0 | basename = strrchr(filename, '/'); |
335 | 0 | basename = basename ? basename + 1 : filename; |
336 | 0 | // Look for Windows path separator as well. |
337 | 0 | filename = strrchr(basename, '\\'); |
338 | 0 | if (filename) { |
339 | 0 | basename = filename + 1; |
340 | 0 | } |
341 | 0 | } |
342 | 0 |
|
343 | 0 | char buffer[128]; // Enough to fit longest js file name from the tree |
344 | 0 | size_t len = SprintfLiteral(buffer, "%s:%u", basename, lineno); |
345 | 0 | if (len < sizeof(buffer)) { |
346 | 0 | mDesiredBufferSize += len + 1; |
347 | 0 |
|
348 | 0 | if (mStackToFill->stack().Capacity() > mStackToFill->stack().Length() && |
349 | 0 | (mStackToFill->strbuffer().Capacity() - |
350 | 0 | mStackToFill->strbuffer().Length()) > len + 1) { |
351 | 0 | // NOTE: We only increment this if we're going to successfully append. |
352 | 0 | mDesiredStackSize += 1; |
353 | 0 | uint32_t start = mStackToFill->strbuffer().Length(); |
354 | 0 | mStackToFill->strbuffer().AppendElements(buffer, len); |
355 | 0 | mStackToFill->strbuffer().AppendElement('\0'); |
356 | 0 | mStackToFill->stack().AppendElement(HangEntryBufOffset(start)); |
357 | 0 | return; |
358 | 0 | } |
359 | 0 | } |
360 | 0 | |
361 | 0 | TryAppendFrame(HangEntryChromeScript()); |
362 | 0 | } |
363 | | |
364 | | } // namespace mozilla |