|
7 | 7 | #include <sched.h> |
8 | 8 | #include <unistd.h> |
9 | 9 | #include <sys/syscall.h> |
| 10 | +#include <sys/resource.h> |
| 11 | +#include <sys/types.h> |
| 12 | +#include <dirent.h> |
| 13 | +#include <fcntl.h> |
10 | 14 | #include <android/log.h> |
11 | 15 | #include <cstdint> |
12 | 16 | #include <cstdio> |
13 | 17 | #include <cstring> |
| 18 | +#include <cstdlib> |
14 | 19 | #include <vector> |
15 | 20 | #include <algorithm> |
16 | 21 | typedef uint8_t byte; |
@@ -545,6 +550,144 @@ OSU_EXPORT int nGetBigCoreMask() { |
545 | 550 | } |
546 | 551 | return mask; |
547 | 552 | } |
| 553 | + |
| 554 | +// ============================================================ |
| 555 | +// Background-thread taming (field-crash mitigation — see below) |
| 556 | +// ============================================================ |
| 557 | +// Android cold-start black-screen / MotionEvent ANR (v177) was root-caused to |
| 558 | +// Veldrid's shader-compile worker running glslang::TPpContext::tokenize deep |
| 559 | +// inside glslang::SetupBuiltinSymbolTable at nice=-10 on a big core, starving |
| 560 | +// the Android main UI thread of CPU at the same moment the Draw thread is |
| 561 | +// draining a 300+-item texture-upload queue. Mono maps .NET |
| 562 | +// ThreadPriority.Highest to nice=-10 for every worker thread it spawns |
| 563 | +// (shader compile, finalizer, network, etc.), which is the display- |
| 564 | +// compositor priority class — inappropriate for CPU-heavy background work. |
| 565 | +// |
| 566 | +// This function walks /proc/self/task, reads each thread's kernel comm, and |
| 567 | +// for any comm matching the Mono-threadpool-worker naming pattern drops the |
| 568 | +// nice value to 0 and (if little_core_mask != 0) pins the thread to the |
| 569 | +// given LITTLE-core subset. Game-loop threads (Update/Draw/Audio/Input), the |
| 570 | +// Android main UI thread (tid == tgid), the calling thread, and a small |
| 571 | +// list of critical ART/system daemons are explicitly left alone. |
| 572 | +// |
| 573 | +// Safe to call repeatedly; subsequent calls are idempotent. Cross-thread |
| 574 | +// setpriority / sched_setaffinity within the same process is allowed for a |
| 575 | +// same-euid caller without CAP_SYS_NICE, so no root required. |
| 576 | +// |
| 577 | +// Returns: number of threads demoted (for diagnostic logging). |
| 578 | +static bool isCommToLeaveAlone(const char* comm) { |
| 579 | + if (!comm || !*comm) return false; |
| 580 | + |
| 581 | + // Game-loop threads created by osu-framework GameThread. These MUST stay |
| 582 | + // at their elevated priority so the render/update/audio/input pipelines |
| 583 | + // aren't starved by Android's scheduler during play. |
| 584 | + static const char* const keep[] = { |
| 585 | + "Update", "Draw", "Audio", "Input", |
| 586 | + "SDLActivity", "HangWatchdog", |
| 587 | + // Known-critical ART / Android daemons; leave to platform defaults. |
| 588 | + "FinalizerDae", "FinalizerWat", "ReferenceQueu", "HeapTaskDaemo", |
| 589 | + "Signal Catche", "Jit thread po", "Profile Saver", "binder:", |
| 590 | + "perfetto", "main", |
| 591 | + // Oboe / AAudio callback threads (priority-critical for audio). |
| 592 | + "AAudio", "OboeAudio", |
| 593 | + }; |
| 594 | + |
| 595 | + for (const char* p : keep) { |
| 596 | + if (std::strncmp(comm, p, std::strlen(p)) == 0) |
| 597 | + return true; |
| 598 | + } |
| 599 | + |
| 600 | + return false; |
| 601 | +} |
| 602 | + |
| 603 | +static bool isCommToDemote(const char* comm) { |
| 604 | + if (!comm) return false; |
| 605 | + |
| 606 | + // Empty comm (unnamed thread) — definitely safe to demote; these are |
| 607 | + // ad-hoc pthread_create workers that inherited nice=-10 from a parent. |
| 608 | + if (*comm == '\0') return true; |
| 609 | + |
| 610 | + // Mono threadpool worker default name: "Thread-<n>". This is the thread |
| 611 | + // that was stuck in glslang::SetupBuiltinSymbolTable in the field |
| 612 | + // tombstone; it's also used for network / shader / JIT helpers. |
| 613 | + if (std::strncmp(comm, "Thread-", 7) == 0) return true; |
| 614 | + |
| 615 | + // OkHttp / Okio network threads — nice=-8 observed in tombstones, no |
| 616 | + // reason to outrank the main UI thread during cold start. |
| 617 | + if (std::strncmp(comm, "OkHttp", 6) == 0) return true; |
| 618 | + if (std::strncmp(comm, "Okio", 4) == 0) return true; |
| 619 | + |
| 620 | + // .NET threadpool default pattern ("pool-", ".NET Thread", "TP"). |
| 621 | + if (std::strncmp(comm, "pool-", 5) == 0) return true; |
| 622 | + if (std::strncmp(comm, ".NET", 4) == 0) return true; |
| 623 | + |
| 624 | + return false; |
| 625 | +} |
| 626 | + |
| 627 | +OSU_EXPORT int nTameBackgroundThreads(int little_core_mask) { |
| 628 | + DIR* dir = opendir("/proc/self/task"); |
| 629 | + if (!dir) return 0; |
| 630 | + |
| 631 | + const pid_t self_tid = (pid_t)syscall(SYS_gettid); |
| 632 | + const pid_t tgid = getpid(); |
| 633 | + |
| 634 | + cpu_set_t cpuset; |
| 635 | + CPU_ZERO(&cpuset); |
| 636 | + bool haveCpuset = false; |
| 637 | + if (little_core_mask != 0) { |
| 638 | + for (int i = 0; i < 32; i++) { |
| 639 | + if ((little_core_mask >> i) & 1) |
| 640 | + CPU_SET(i, &cpuset); |
| 641 | + } |
| 642 | + haveCpuset = CPU_COUNT(&cpuset) > 0; |
| 643 | + } |
| 644 | + |
| 645 | + int demoted = 0; |
| 646 | + struct dirent* ent; |
| 647 | + while ((ent = readdir(dir)) != nullptr) { |
| 648 | + if (ent->d_name[0] < '0' || ent->d_name[0] > '9') continue; |
| 649 | + |
| 650 | + pid_t tid = (pid_t)std::atoi(ent->d_name); |
| 651 | + if (tid <= 0) continue; |
| 652 | + if (tid == self_tid) continue; |
| 653 | + // Never touch the Android main UI thread — Android's input dispatcher |
| 654 | + // reads from it, and any priority/affinity mutation here is exactly |
| 655 | + // the class of change that causes a 10s MotionEvent ANR. |
| 656 | + if (tid == tgid) continue; |
| 657 | + |
| 658 | + char comm_path[64]; |
| 659 | + std::snprintf(comm_path, sizeof(comm_path), "/proc/self/task/%d/comm", (int)tid); |
| 660 | + int fd = open(comm_path, O_RDONLY | O_CLOEXEC); |
| 661 | + if (fd < 0) continue; |
| 662 | + |
| 663 | + char comm[32] = {0}; |
| 664 | + ssize_t n = read(fd, comm, sizeof(comm) - 1); |
| 665 | + close(fd); |
| 666 | + if (n <= 0) continue; |
| 667 | + // Strip trailing newline that the kernel appends. |
| 668 | + if (comm[n - 1] == '\n') comm[n - 1] = '\0'; |
| 669 | + |
| 670 | + if (isCommToLeaveAlone(comm)) continue; |
| 671 | + if (!isCommToDemote(comm)) continue; |
| 672 | + |
| 673 | + bool changed = false; |
| 674 | + |
| 675 | + // Raise nice from whatever it is (often -10 for Mono ThreadPriority.Highest) |
| 676 | + // to 0. setpriority(PRIO_PROCESS, tid, 0) is a de-elevation and |
| 677 | + // therefore does not require CAP_SYS_NICE for a same-euid caller. |
| 678 | + if (setpriority(PRIO_PROCESS, tid, 0) == 0) |
| 679 | + changed = true; |
| 680 | + |
| 681 | + if (haveCpuset) { |
| 682 | + if (sched_setaffinity(tid, sizeof(cpu_set_t), &cpuset) == 0) |
| 683 | + changed = true; |
| 684 | + } |
| 685 | + |
| 686 | + if (changed) demoted++; |
| 687 | + } |
| 688 | + closedir(dir); |
| 689 | + return demoted; |
| 690 | +} |
548 | 691 | } |
549 | 692 |
|
550 | 693 | #include <android/performance_hint.h> |
|
0 commit comments