GeckoJavaSampler.java (31599B)
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.gecko; 7 8 import android.content.ComponentName; 9 import android.content.Intent; 10 import android.os.Build; 11 import android.os.Looper; 12 import android.os.Process; 13 import android.os.SystemClock; 14 import android.util.Log; 15 import androidx.annotation.GuardedBy; 16 import androidx.annotation.NonNull; 17 import androidx.annotation.Nullable; 18 import androidx.core.content.ContextCompat; 19 import java.util.ArrayList; 20 import java.util.Arrays; 21 import java.util.Collections; 22 import java.util.HashSet; 23 import java.util.List; 24 import java.util.Locale; 25 import java.util.Objects; 26 import java.util.Queue; 27 import java.util.Set; 28 import java.util.concurrent.Executors; 29 import java.util.concurrent.LinkedBlockingQueue; 30 import java.util.concurrent.ScheduledExecutorService; 31 import java.util.concurrent.ScheduledFuture; 32 import java.util.concurrent.TimeUnit; 33 import java.util.concurrent.atomic.AtomicReference; 34 import org.mozilla.gecko.annotation.WrapForJNI; 35 import org.mozilla.gecko.mozglue.JNIObject; 36 import org.mozilla.geckoview.GeckoResult; 37 38 /** 39 * Takes samples and adds markers for Java threads for the Gecko profiler. 40 * 41 * <p>This class is thread safe because it uses synchronized on accesses to its mutable state. One 42 * exception is {@link #isProfilerActive()}: see the javadoc for details. 43 */ 44 public class GeckoJavaSampler { 45 46 private static final String LOGTAG = "GeckoJavaSampler"; 47 48 private static final String PROFILER_SERVICE_CLASS_NAME = 49 "org.mozilla.fenix.perf.ProfilerService"; 50 private static final String PROFILER_SERVICE_ACTION = "mozilla.perf.action.START_PROFILING"; 51 public static final String INTENT_PROFILER_STATE_CHANGED = 52 "org.mozilla.fenix.PROFILER_STATE_CHANGED"; 53 54 /** 55 * The thread ID to use for the main thread instead of its true thread ID. 56 * 57 * <p>The main thread is sampled twice: once for native code and once on the JVM. The native 58 * version uses the thread's id so we replace it to avoid a collision. We use this thread ID 59 * because it's unlikely any other thread currently has it. We can't use 0 because 0 is considered 60 * "unspecified" in native code: 61 * https://searchfox.org/mozilla-central/rev/d4ebb53e719b913afdbcf7c00e162f0e96574701/mozglue/baseprofiler/public/BaseProfilerUtils.h#194 62 */ 63 private static final long REPLACEMENT_MAIN_THREAD_ID = 1; 64 65 /** 66 * The thread name to use for the main thread instead of its true thread name. The name is "main", 67 * which is ambiguous with the JS main thread, so we rename it to match the C++ replacement. We 68 * expect our code to later add a suffix to avoid a collision with the C++ thread name. See {@link 69 * #REPLACEMENT_MAIN_THREAD_ID} for related details. 70 */ 71 private static final String REPLACEMENT_MAIN_THREAD_NAME = "AndroidUI"; 72 73 @GuardedBy("GeckoJavaSampler.class") 74 private static SamplingRunnable sSamplingRunnable; 75 76 @GuardedBy("GeckoJavaSampler.class") 77 private static ScheduledExecutorService sSamplingScheduler; 78 79 // See isProfilerActive for details on the AtomicReference. 80 @GuardedBy("GeckoJavaSampler.class") 81 private static final AtomicReference<ScheduledFuture<?>> sSamplingFuture = 82 new AtomicReference<>(); 83 84 private static final MarkerStorage sMarkerStorage = new MarkerStorage(); 85 86 /** 87 * Returns true if profiler is running and unpaused at the moment which means it's allowed to add 88 * a marker. 89 * 90 * <p>Thread policy: we want this method to be inexpensive (i.e. non-blocking) because we want to 91 * be able to use it in performance-sensitive code. That's why we rely on an AtomicReference. If 92 * this requirement didn't exist, the AtomicReference could be removed because the class thread 93 * policy is to call synchronized on mutable state access. 94 */ 95 public static boolean isProfilerActive() { 96 // This value will only be present if the profiler is started and not paused. 97 return sSamplingFuture.get() != null; 98 } 99 100 // Use the same timer primitive as the profiler 101 // to get a perfect sample syncing. 102 @WrapForJNI 103 private static native double getProfilerTime(); 104 105 /** Try to get the profiler time. Returns null if profiler is not running. */ 106 public static @Nullable Double tryToGetProfilerTime() { 107 if (!isProfilerActive()) { 108 // Android profiler hasn't started yet. 109 return null; 110 } 111 if (!GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) { 112 // getProfilerTime is not available yet; either libs are not loaded, 113 // or profiling hasn't started on the Gecko side yet 114 return null; 115 } 116 117 return getProfilerTime(); 118 } 119 120 /** 121 * A data container for a profiler sample. This class is effectively immutable (i.e. technically 122 * mutable but never mutated after construction) so is thread safe *if it is safely published* 123 * (see Java Concurrency in Practice, 2nd Ed., Section 3.5.3 for safe publication idioms). 124 */ 125 private static class Sample { 126 public final long mThreadId; 127 public final Frame[] mFrames; 128 public final double mTime; 129 public final long mJavaTime; // non-zero if Android system time is used 130 131 public Sample(final long aThreadId, final StackTraceElement[] aStack) { 132 mThreadId = aThreadId; 133 mFrames = new Frame[aStack.length]; 134 mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0; 135 136 // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded, 137 // or profiling hasn't started on the Gecko side yet 138 mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0; 139 140 for (int i = 0; i < aStack.length; i++) { 141 mFrames[aStack.length - 1 - i] = 142 new Frame(aStack[i].getMethodName(), aStack[i].getClassName()); 143 } 144 } 145 } 146 147 /** 148 * A container for the metadata around a call in a stack. This class is thread safe by being 149 * immutable. 150 */ 151 private static class Frame { 152 public final String methodName; 153 public final String className; 154 155 private Frame(final String methodName, final String className) { 156 this.methodName = methodName; 157 this.className = className; 158 } 159 } 160 161 /** A data container for thread metadata. */ 162 private static class ThreadInfo { 163 private final long mId; 164 private final String mName; 165 166 public ThreadInfo(final long mId, final String mName) { 167 this.mId = mId; 168 this.mName = mName; 169 } 170 171 @WrapForJNI 172 public long getId() { 173 return mId; 174 } 175 176 @WrapForJNI 177 public String getName() { 178 return mName; 179 } 180 } 181 182 /** 183 * A data container for metadata around a marker. This class is thread safe by being immutable. 184 */ 185 private static class Marker extends JNIObject { 186 /** The id of the thread this marker was captured on. */ 187 private final long mThreadId; 188 189 /** Name of the marker */ 190 private final String mMarkerName; 191 192 /** Either start time for the duration markers or time for a point-in-time markers. */ 193 private final double mTime; 194 195 /** 196 * A fallback field of {@link #mTime} but it only exists when {@link #getProfilerTime()} is 197 * failed. It is non-zero if Android time is used. 198 */ 199 private final long mJavaTime; 200 201 /** End time for the duration markers. It's zero for point-in-time markers. */ 202 private final double mEndTime; 203 204 /** 205 * A fallback field of {@link #mEndTime} but it only exists when {@link #getProfilerTime()} is 206 * failed. It is non-zero if Android time is used. 207 */ 208 private final long mEndJavaTime; 209 210 /** A nullable additional information field for the marker. */ 211 private @Nullable final String mText; 212 213 /** 214 * Constructor for the Marker class. It initializes different kinds of markers depending on the 215 * parameters. Here are some combinations to create different kinds of markers: 216 * 217 * <p>If you want to create a marker that points a single point in time: <code> 218 * new Marker("name", null, null, null)</code> to implicitly get the time when this marker is 219 * added, or <code>new Marker("name", null, endTime, null)</code> to use an explicit time as an 220 * end time retrieved from {@link #tryToGetProfilerTime()}. 221 * 222 * <p>If you want to create a marker that has a start and end time: <code> 223 * new Marker("name", startTime, null, null)</code> to implicitly get the end time when this 224 * marker is added, or <code>new Marker("name", startTime, endTime, null)</code> to explicitly 225 * give the marker start and end time retrieved from {@link #tryToGetProfilerTime()}. 226 * 227 * <p>Last parameter is optional and can be given with any combination. This gives users the 228 * ability to add more context into a marker. 229 * 230 * @param aThreadId The id of the thread this marker was captured on. 231 * @param aMarkerName Identifier of the marker as a string. 232 * @param aStartTime Start time as Double. It can be null if you want to mark a point of time. 233 * @param aEndTime End time as Double. If it's null, this function implicitly gets the end time. 234 * @param aText An optional string field for more information about the marker. 235 */ 236 public Marker( 237 final long aThreadId, 238 @NonNull final String aMarkerName, 239 @Nullable final Double aStartTime, 240 @Nullable final Double aEndTime, 241 @Nullable final String aText) { 242 mThreadId = getAdjustedThreadId(aThreadId); 243 mMarkerName = aMarkerName; 244 mText = aText; 245 246 if (aStartTime != null) { 247 // Start time is provided. This is an interval marker. 248 mTime = aStartTime; 249 mJavaTime = 0; 250 if (aEndTime != null) { 251 // End time is also provided. 252 mEndTime = aEndTime; 253 mEndJavaTime = 0; 254 } else { 255 // End time is not provided. Get the profiler time now and use it. 256 mEndTime = 257 GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0; 258 259 // if mEndTime == 0, getProfilerTime is not available yet; either libs are not loaded, 260 // or profiling hasn't started on the Gecko side yet 261 mEndJavaTime = mEndTime == 0.0d ? SystemClock.elapsedRealtime() : 0; 262 } 263 264 } else { 265 // Start time is not provided. This is point-in-time marker. 266 mEndTime = 0; 267 mEndJavaTime = 0; 268 269 if (aEndTime != null) { 270 // End time is also provided. Use that to point the time. 271 mTime = aEndTime; 272 mJavaTime = 0; 273 } else { 274 mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0; 275 276 // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded, 277 // or profiling hasn't started on the Gecko side yet 278 mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0; 279 } 280 } 281 } 282 283 @WrapForJNI 284 @Override // JNIObject 285 protected native void disposeNative(); 286 287 @WrapForJNI 288 public double getStartTime() { 289 if (mJavaTime != 0) { 290 return (mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime(); 291 } 292 return mTime; 293 } 294 295 @WrapForJNI 296 public double getEndTime() { 297 if (mEndJavaTime != 0) { 298 return (mEndJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime(); 299 } 300 return mEndTime; 301 } 302 303 @WrapForJNI 304 public long getThreadId() { 305 return mThreadId; 306 } 307 308 @WrapForJNI 309 public @NonNull String getMarkerName() { 310 return mMarkerName; 311 } 312 313 @WrapForJNI 314 public @Nullable String getMarkerText() { 315 return mText; 316 } 317 } 318 319 /** 320 * Public method to add a new marker to Gecko profiler. This can be used to add a marker *inside* 321 * the geckoview code, but ideally ProfilerController methods should be used instead. 322 * 323 * @see Marker#Marker(long, String, Double, Double, String) for information about the parameter 324 * options. 325 */ 326 public static void addMarker( 327 @NonNull final String aMarkerName, 328 @Nullable final Double aStartTime, 329 @Nullable final Double aEndTime, 330 @Nullable final String aText) { 331 sMarkerStorage.addMarker(aMarkerName, aStartTime, aEndTime, aText); 332 } 333 334 /** 335 * A routine to store profiler samples. This class is thread safe because it synchronizes access 336 * to its mutable state. 337 */ 338 private static class SamplingRunnable implements Runnable { 339 private final long mMainThreadId = Looper.getMainLooper().getThread().getId(); 340 341 // Sampling interval that is used by start and unpause 342 public final int mInterval; 343 private final int mSampleCount; 344 345 @GuardedBy("GeckoJavaSampler.class") 346 private boolean mBufferOverflowed = false; 347 348 @GuardedBy("GeckoJavaSampler.class") 349 private @NonNull final List<Thread> mThreadsToProfile; 350 351 @GuardedBy("GeckoJavaSampler.class") 352 private final Sample[] mSamples; 353 354 @GuardedBy("GeckoJavaSampler.class") 355 private int mSamplePos; 356 357 public SamplingRunnable( 358 @NonNull final List<Thread> aThreadsToProfile, 359 final int aInterval, 360 final int aSampleCount) { 361 mThreadsToProfile = aThreadsToProfile; 362 // Sanity check of sampling interval. 363 mInterval = Math.max(1, aInterval); 364 mSampleCount = aSampleCount; 365 mSamples = new Sample[mSampleCount]; 366 mSamplePos = 0; 367 } 368 369 @Override 370 public void run() { 371 synchronized (GeckoJavaSampler.class) { 372 // To minimize allocation in the critical section, we use a traditional for loop instead of 373 // a for each (i.e. `elem : coll`) loop because that allocates an iterator. 374 // 375 // We won't capture threads that are started during profiling because we iterate through an 376 // unchanging list of threads (bug 1759550). 377 for (int i = 0; i < mThreadsToProfile.size(); i++) { 378 final Thread thread = mThreadsToProfile.get(i); 379 380 // getStackTrace will return an empty trace if the thread is not alive: we call continue 381 // to avoid wasting space in the buffer for an empty sample. 382 final StackTraceElement[] stackTrace = thread.getStackTrace(); 383 if (stackTrace.length == 0) { 384 continue; 385 } 386 387 mSamples[mSamplePos] = new Sample(thread.getId(), stackTrace); 388 mSamplePos += 1; 389 if (mSamplePos == mSampleCount) { 390 // Sample array is full now, go back to start of 391 // the array and override old samples 392 mSamplePos = 0; 393 mBufferOverflowed = true; 394 } 395 } 396 } 397 } 398 399 private Sample getSample(final int aSampleId) { 400 synchronized (GeckoJavaSampler.class) { 401 if (aSampleId >= mSampleCount) { 402 // Return early because there is no more sample left. 403 return null; 404 } 405 406 int samplePos = aSampleId; 407 if (mBufferOverflowed) { 408 // This is a circular buffer and the buffer is overflowed. Start 409 // of the buffer is mSamplePos now. Calculate the real index. 410 samplePos = (samplePos + mSamplePos) % mSampleCount; 411 } 412 413 // Since the array elements are initialized to null, it will return 414 // null whenever we access to an element that's not been written yet. 415 // We want it to return null in that case, so it's okay. 416 return mSamples[samplePos]; 417 } 418 } 419 } 420 421 /** 422 * Returns the sample with the given sample ID. 423 * 424 * <p>Thread safety code smell: this method call is synchronized but this class returns a 425 * reference to an effectively immutable object so that the reference is accessible after 426 * synchronization ends. It's unclear if this is thread safe. However, this is safe with the 427 * current callers (because they are all synchronized and don't leak the Sample) so we don't 428 * investigate it further. 429 */ 430 private static synchronized Sample getSample(final int aSampleId) { 431 return sSamplingRunnable.getSample(aSampleId); 432 } 433 434 @WrapForJNI 435 public static Marker pollNextMarker() { 436 return sMarkerStorage.pollNextMarker(); 437 } 438 439 @WrapForJNI 440 public static synchronized int getRegisteredThreadCount() { 441 return sSamplingRunnable.mThreadsToProfile.size(); 442 } 443 444 @WrapForJNI 445 public static synchronized ThreadInfo getRegisteredThreadInfo(final int aIndex) { 446 final Thread thread = sSamplingRunnable.mThreadsToProfile.get(aIndex); 447 448 // See REPLACEMENT_MAIN_THREAD_NAME for why we do this. 449 String adjustedThreadName = 450 thread.getId() == sSamplingRunnable.mMainThreadId 451 ? REPLACEMENT_MAIN_THREAD_NAME 452 : thread.getName(); 453 454 // To distinguish JVM threads from native threads, we append a JVM-specific suffix. 455 adjustedThreadName += " (JVM)"; 456 return new ThreadInfo(getAdjustedThreadId(thread.getId()), adjustedThreadName); 457 } 458 459 @WrapForJNI 460 public static synchronized long getThreadId(final int aSampleId) { 461 final Sample sample = getSample(aSampleId); 462 return getAdjustedThreadId(sample != null ? sample.mThreadId : 0); 463 } 464 465 private static synchronized long getAdjustedThreadId(final long threadId) { 466 // See REPLACEMENT_MAIN_THREAD_ID for why we do this. 467 return threadId == sSamplingRunnable.mMainThreadId ? REPLACEMENT_MAIN_THREAD_ID : threadId; 468 } 469 470 @WrapForJNI 471 public static synchronized double getSampleTime(final int aSampleId) { 472 final Sample sample = getSample(aSampleId); 473 if (sample != null) { 474 if (sample.mJavaTime != 0) { 475 return (sample.mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime(); 476 } 477 return sample.mTime; 478 } 479 return 0; 480 } 481 482 @WrapForJNI 483 public static synchronized String getFrameName(final int aSampleId, final int aFrameId) { 484 final Sample sample = getSample(aSampleId); 485 if (sample != null && aFrameId < sample.mFrames.length) { 486 final Frame frame = sample.mFrames[aFrameId]; 487 if (frame == null) { 488 return null; 489 } 490 return frame.className + "." + frame.methodName + "()"; 491 } 492 return null; 493 } 494 495 /** 496 * A start/stop-aware container for storing profiler markers. 497 * 498 * <p>This class is thread safe: see {@link #mMarkers} and other member variables for the 499 * threading policy. Start/stop are guaranteed to execute in the order they are called but other 500 * methods do not have such ordering guarantees. 501 */ 502 private static class MarkerStorage { 503 /** 504 * The underlying storage for the markers. This field maintains thread safety without using 505 * synchronized everywhere by: 506 * <li>- using volatile to allow non-blocking reads 507 * <li>- leveraging a thread safe collection when accessing the underlying data 508 * <li>- looping until success for compound read-write operations 509 */ 510 private volatile Queue<Marker> mMarkers; 511 512 /** 513 * The thread ids of the threads we're profiling. This field maintains thread safety by writing 514 * a read-only value to this volatile field before concurrency begins and only reading it during 515 * concurrent sections. 516 */ 517 private volatile Set<Long> mProfiledThreadIds = Collections.emptySet(); 518 519 MarkerStorage() {} 520 521 public synchronized void start(final int aMarkerCount, final List<Thread> aProfiledThreads) { 522 if (this.mMarkers != null) { 523 return; 524 } 525 this.mMarkers = new LinkedBlockingQueue<>(aMarkerCount); 526 527 final Set<Long> profiledThreadIds = new HashSet<>(aProfiledThreads.size()); 528 for (final Thread thread : aProfiledThreads) { 529 profiledThreadIds.add(thread.getId()); 530 } 531 532 // We use a temporary collection, rather than mutating the collection within the member 533 // variable, to ensure the collection is fully written before the state is made available to 534 // all threads via the volatile write into the member variable. This collection must be 535 // read-only for it to remain thread safe. 536 mProfiledThreadIds = Collections.unmodifiableSet(profiledThreadIds); 537 } 538 539 public synchronized void stop() { 540 if (this.mMarkers == null) { 541 return; 542 } 543 this.mMarkers = null; 544 mProfiledThreadIds = Collections.emptySet(); 545 } 546 547 private void addMarker( 548 @NonNull final String aMarkerName, 549 @Nullable final Double aStartTime, 550 @Nullable final Double aEndTime, 551 @Nullable final String aText) { 552 final Queue<Marker> markersQueue = this.mMarkers; 553 if (markersQueue == null) { 554 // Profiler is not active. 555 return; 556 } 557 558 final long threadId = Thread.currentThread().getId(); 559 if (!mProfiledThreadIds.contains(threadId)) { 560 return; 561 } 562 563 final Marker newMarker = new Marker(threadId, aMarkerName, aStartTime, aEndTime, aText); 564 boolean successful = markersQueue.offer(newMarker); 565 while (!successful) { 566 // Marker storage is full, remove the head and add again. 567 markersQueue.poll(); 568 successful = markersQueue.offer(newMarker); 569 } 570 } 571 572 private Marker pollNextMarker() { 573 final Queue<Marker> markersQueue = this.mMarkers; 574 if (markersQueue == null) { 575 // Profiler is not active. 576 return null; 577 } 578 // Retrieve and return the head of this queue. 579 // Returns null if the queue is empty. 580 return markersQueue.poll(); 581 } 582 } 583 584 @WrapForJNI 585 public static void start( 586 @NonNull final Object[] aFilters, final int aInterval, final int aEntryCount) { 587 synchronized (GeckoJavaSampler.class) { 588 if (sSamplingRunnable != null) { 589 return; 590 } 591 592 final ScheduledFuture<?> future = sSamplingFuture.get(); 593 if (future != null && !future.isDone()) { 594 return; 595 } 596 597 // Setting a limit of 120000 (2 mins with 1ms interval) for samples and markers for now 598 // to make sure we are not allocating too much. 599 final int limitedEntryCount = Math.min(aEntryCount, 120000); 600 601 final List<Thread> threadsToProfile = getThreadsToProfile(aFilters); 602 if (threadsToProfile.size() < 1) { 603 throw new IllegalStateException("Expected >= 1 thread to profile (main thread)."); 604 } 605 Log.i(LOGTAG, "Number of threads to profile: " + threadsToProfile.size()); 606 607 sSamplingRunnable = new SamplingRunnable(threadsToProfile, aInterval, limitedEntryCount); 608 sMarkerStorage.start(limitedEntryCount, threadsToProfile); 609 sSamplingScheduler = Executors.newSingleThreadScheduledExecutor(); 610 sSamplingFuture.set( 611 sSamplingScheduler.scheduleAtFixedRate( 612 sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS)); 613 } 614 } 615 616 private static @NonNull List<Thread> getThreadsToProfile(final Object[] aFilters) { 617 // Clean up filters. 618 final List<String> cleanedFilters = new ArrayList<>(); 619 for (final Object rawFilter : aFilters) { 620 // aFilters is a String[] but jni can only accept Object[] so we're forced to cast. 621 // 622 // We could pass the lowercased filters from native code but it may not handle lowercasing the 623 // same way Java does so we lower case here so it's consistent later when we lower case the 624 // thread name and compare against it. 625 final String filter = ((String) rawFilter).trim().toLowerCase(Locale.US); 626 627 // If the filter is empty, it's not meaningful: skip. 628 if (filter.isEmpty()) { 629 continue; 630 } 631 632 cleanedFilters.add(filter); 633 } 634 635 final ThreadGroup rootThreadGroup = getRootThreadGroup(); 636 final Thread[] activeThreads = getActiveThreads(rootThreadGroup); 637 final Thread mainThread = Looper.getMainLooper().getThread(); 638 639 // We model these catch-all filters after the C++ code (which we should eventually deduplicate): 640 // https://searchfox.org/mozilla-central/rev/b0779bcc485dc1c04334dfb9ea024cbfff7b961a/tools/profiler/core/platform.cpp#778-801 641 if (cleanedFilters.contains("*") || doAnyFiltersMatchPid(cleanedFilters, Process.myPid())) { 642 final List<Thread> activeThreadList = new ArrayList<>(); 643 Collections.addAll(activeThreadList, activeThreads); 644 if (!activeThreadList.contains(mainThread)) { 645 activeThreadList.add(mainThread); // see below for why this is necessary. 646 } 647 return activeThreadList; 648 } 649 650 // We always want to profile the main thread. We're not certain getActiveThreads returns 651 // all active threads since we've observed that getActiveThreads doesn't include the main thread 652 // during xpcshell tests even though it's alive (bug 1760716). We intentionally don't rely on 653 // that method to add the main thread here. 654 final List<Thread> threadsToProfile = new ArrayList<>(); 655 threadsToProfile.add(mainThread); 656 657 for (final Thread thread : activeThreads) { 658 if (shouldProfileThread(thread, cleanedFilters, mainThread)) { 659 threadsToProfile.add(thread); 660 } 661 } 662 return threadsToProfile; 663 } 664 665 private static boolean shouldProfileThread( 666 final Thread aThread, final List<String> aFilters, final Thread aMainThread) { 667 final String threadName = aThread.getName().trim().toLowerCase(Locale.US); 668 if (threadName.isEmpty()) { 669 return false; // We can't match against a thread with no name: skip. 670 } 671 672 if (aThread.equals(aMainThread)) { 673 return false; // We've already added the main thread outside of this method. 674 } 675 676 for (final String filter : aFilters) { 677 // In order to generically support thread pools with thread names like "arch_disk_io_0" (the 678 // kotlin IO dispatcher), we check if the filter is inside the thread name (e.g. a filter of 679 // "io" will match all of the threads in that pool) rather than an equality check. 680 if (threadName.contains(filter)) { 681 return true; 682 } 683 } 684 685 return false; 686 } 687 688 private static boolean doAnyFiltersMatchPid( 689 @NonNull final List<String> aFilters, final long aPid) { 690 final String prefix = "pid:"; 691 for (final String filter : aFilters) { 692 if (!filter.startsWith(prefix)) { 693 continue; 694 } 695 696 try { 697 final long filterPid = Long.parseLong(filter.substring(prefix.length())); 698 if (filterPid == aPid) { 699 return true; 700 } 701 } catch (final NumberFormatException e) { 702 /* do nothing. */ 703 } 704 } 705 706 return false; 707 } 708 709 private static @NonNull Thread[] getActiveThreads(final @NonNull ThreadGroup rootThreadGroup) { 710 // We need the root thread group to get all of the active threads because of how 711 // ThreadGroup.enumerate works. 712 // 713 // ThreadGroup.enumerate is inherently racey so we loop until we capture all of the active 714 // threads. We can only detect if we didn't capture all of the threads if the number of threads 715 // found (the value returned by enumerate) is smaller than the array we're capturing them in. 716 // Therefore, we make the array slightly larger than the known number of threads. 717 Thread[] allThreads; 718 int threadsFound; 719 do { 720 allThreads = new Thread[rootThreadGroup.activeCount() + 15]; 721 threadsFound = rootThreadGroup.enumerate(allThreads, /* recurse */ true); 722 } while (threadsFound >= allThreads.length); 723 724 // There will be more indices in the array than threads and these will be set to null. We remove 725 // the null values to minimize bugs. 726 return Arrays.copyOfRange(allThreads, 0, threadsFound); 727 } 728 729 private static @NonNull ThreadGroup getRootThreadGroup() { 730 // Assert non-null: getThreadGroup only returns null for dead threads but the current thread 731 // can't be dead. 732 ThreadGroup parentGroup = Objects.requireNonNull(Thread.currentThread().getThreadGroup()); 733 734 ThreadGroup group = null; 735 while (parentGroup != null) { 736 group = parentGroup; 737 parentGroup = group.getParent(); 738 } 739 return group; 740 } 741 742 @WrapForJNI 743 public static void pauseSampling() { 744 synchronized (GeckoJavaSampler.class) { 745 final ScheduledFuture<?> future = sSamplingFuture.getAndSet(null); 746 future.cancel(false /* mayInterruptIfRunning */); 747 } 748 } 749 750 @WrapForJNI 751 public static void unpauseSampling() { 752 synchronized (GeckoJavaSampler.class) { 753 if (sSamplingFuture.get() != null) { 754 return; 755 } 756 sSamplingFuture.set( 757 sSamplingScheduler.scheduleAtFixedRate( 758 sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS)); 759 } 760 } 761 762 @WrapForJNI 763 public static void stop() { 764 synchronized (GeckoJavaSampler.class) { 765 if (sSamplingRunnable == null) { 766 return; 767 } 768 769 Log.i( 770 LOGTAG, 771 "Profiler stopping. Sample array position: " 772 + sSamplingRunnable.mSamplePos 773 + ". Overflowed? " 774 + sSamplingRunnable.mBufferOverflowed); 775 776 try { 777 sSamplingScheduler.shutdown(); 778 // 1s is enough to wait shutdown. 779 sSamplingScheduler.awaitTermination(1000, TimeUnit.MILLISECONDS); 780 } catch (final InterruptedException e) { 781 Log.e(LOGTAG, "Sampling scheduler isn't terminated. Last sampling data might be broken."); 782 sSamplingScheduler.shutdownNow(); 783 } 784 sSamplingScheduler = null; 785 sSamplingRunnable = null; 786 sSamplingFuture.set(null); 787 sMarkerStorage.stop(); 788 } 789 } 790 791 /** 792 * Notifies Fenix layer about profiler state changes by broadcasting the new state. This is called 793 * from native code whenever the profiler starts or stops, ensuring that the Fenix repository is 794 * always synchronized with the actual native profiler state. 795 * 796 * @param isActive true if the profiler is now active, false if it stopped 797 */ 798 @WrapForJNI 799 public static void notifyProfilerStateChanged(final boolean isActive) { 800 if (isActive) { 801 final ComponentName componentName = 802 new ComponentName(GeckoAppShell.getApplicationContext(), PROFILER_SERVICE_CLASS_NAME); 803 final Intent serviceIntent = new Intent(); 804 serviceIntent.setComponent(componentName); 805 serviceIntent.setAction(PROFILER_SERVICE_ACTION); 806 ContextCompat.startForegroundService(GeckoAppShell.getApplicationContext(), serviceIntent); 807 } 808 809 final Intent intent = new Intent(INTENT_PROFILER_STATE_CHANGED); 810 intent.putExtra("isActive", isActive); 811 intent.setPackage(GeckoAppShell.getApplicationContext().getPackageName()); 812 final String permission = 813 GeckoAppShell.getApplicationContext().getPackageName() + ".permission.PROFILER_INTERNAL"; 814 GeckoAppShell.getApplicationContext().sendBroadcast(intent, permission); 815 } 816 817 @WrapForJNI(dispatchTo = "gecko", stubName = "StartProfiler") 818 private static native void startProfilerNative(String[] aFilters, String[] aFeaturesArr); 819 820 @WrapForJNI(dispatchTo = "gecko", stubName = "StopProfiler") 821 private static native void stopProfilerNative(GeckoResult<byte[]> aResult); 822 823 public static void startProfiler(final String[] aFilters, final String[] aFeaturesArr) { 824 startProfilerNative(aFilters, aFeaturesArr); 825 } 826 827 public static GeckoResult<byte[]> stopProfiler() { 828 final GeckoResult<byte[]> result = new GeckoResult<byte[]>(); 829 stopProfilerNative(result); 830 return result; 831 } 832 833 /** Returns the device brand and model as a string. */ 834 @WrapForJNI 835 public static String getDeviceInformation() { 836 final StringBuilder sb = new StringBuilder(Build.BRAND); 837 sb.append(" "); 838 sb.append(Build.MODEL); 839 return sb.toString(); 840 } 841 }