mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-15 15:30:29 +02:00
The core and the headless tools no longer use mutexes, condition variables, spinlocks, std::atomic, std::thread or volatile flags. Threads hand off through libretro-common's retro_atomic and rthreads plus a few small pieces on top of them in Common/Thread: Thread, MpscQueue, EventCounter, ParkingLot waits and ReaderGate. Every wait blocks until it's woken; nothing polls on a timer. State that only one thread touches just loses its lock. The PSP filesystem, sceIo and the savedata dialog now belong to the emulation thread: async reads and writes run when they're queued and complete later in emulated time, and the IO and savedata threads are gone. The friend finder only talks to the adhoc server and hands what it gets to the emulation thread. GPU init always runs on the thread that starts the game. The Vulkan render and compile queues, pipeline and render pass caches, delete list and SPIR-V cache, the GL render queue, the software renderer's jit caches, the file loader caches, the web debugger, UPnP, input, savestates, logging and the GE debugger all hand off through queues and atomics. The IR jit's spinning profiler thread goes. A CPU thread with nothing to run now sleeps until work is queued instead of spinning. Headless with --debugger waits for a client while stopped, and ends the run once every client that connected has left. Web server shutdown keeps servicing the CPU queue until the server has finished.
275 lines
7.5 KiB
C++
275 lines
7.5 KiB
C++
// Ultra-lightweight category profiler with history.
|
|
|
|
#include <vector>
|
|
#include <cstring>
|
|
|
|
#include <retro_atomic.h>
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#include "Common/Render/DrawBuffer.h"
|
|
|
|
#include "Common/TimeUtil.h"
|
|
#include "Common/Profiler/Profiler.h"
|
|
#include "Common/Log.h"
|
|
|
|
#define MAX_CATEGORIES 64 // Can be any number, represents max profiled names.
|
|
#define MAX_DEPTH 16 // Can be any number, represents max nesting depth of profiled names.
|
|
#if PPSSPP_PLATFORM(IOS) && defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0
|
|
// iOS did not support C++ thread_local before iOS 9
|
|
#define MAX_THREADS 1 // Can be any number, represents concurrent threads calling the profiler.
|
|
#else
|
|
#define MAX_THREADS 4 // Can be any number, represents concurrent threads calling the profiler.
|
|
#endif
|
|
#define HISTORY_SIZE 128 // Must be power of 2
|
|
|
|
#ifndef _DEBUG
|
|
// If the compiler can collapse identical strings, we don't even need the strcmp.
|
|
#define UNIFIED_CONST_STR
|
|
#endif
|
|
|
|
// Claimed with a CAS by the first thread to enter that category.
|
|
struct Category {
|
|
retro_atomic_ptr_t name;
|
|
};
|
|
|
|
struct CategoryFrame {
|
|
CategoryFrame() {
|
|
memset(time_taken, 0, sizeof(time_taken));
|
|
memset(count, 0, sizeof(count));
|
|
}
|
|
float time_taken[MAX_CATEGORIES];
|
|
int count[MAX_CATEGORIES];
|
|
};
|
|
|
|
struct Profiler {
|
|
int historyPos;
|
|
int depth[MAX_THREADS];
|
|
int parentCategory[MAX_THREADS][MAX_DEPTH];
|
|
double eventStart[MAX_THREADS][MAX_CATEGORIES];
|
|
double curFrameStart;
|
|
};
|
|
|
|
// Each thread only touches its own slots of profiler and history.
|
|
static Profiler profiler;
|
|
static Category categories[MAX_CATEGORIES];
|
|
static retro_atomic_int_t threadIdAfterLast{ 0 };
|
|
static CategoryFrame *history;
|
|
#if MAX_THREADS > 1
|
|
thread_local int profilerThreadId = -1;
|
|
#else
|
|
static int profilerThreadId = 0;
|
|
#endif
|
|
|
|
void internal_profiler_init() {
|
|
memset(&profiler, 0, sizeof(profiler));
|
|
#if MAX_THREADS == 1
|
|
retro_atomic_store_release_int(&threadIdAfterLast, 1);
|
|
#endif
|
|
for (int i = 0; i < MAX_THREADS; i++) {
|
|
for (int j = 0; j < MAX_DEPTH; j++) {
|
|
profiler.parentCategory[i][j] = -1;
|
|
}
|
|
}
|
|
history = new CategoryFrame[HISTORY_SIZE * MAX_THREADS];
|
|
}
|
|
|
|
static int internal_profiler_find_thread() {
|
|
int thread_id = profilerThreadId;
|
|
if (thread_id != -1) {
|
|
return thread_id;
|
|
}
|
|
|
|
thread_id = retro_atomic_fetch_add_int(&threadIdAfterLast, 1);
|
|
if (thread_id >= MAX_THREADS) {
|
|
// Just keep reusing the last one.
|
|
thread_id = MAX_THREADS - 1;
|
|
}
|
|
profilerThreadId = thread_id;
|
|
return thread_id;
|
|
}
|
|
|
|
static int NumThreads() {
|
|
int n = retro_atomic_load_acquire_int(&threadIdAfterLast);
|
|
return n < MAX_THREADS ? n : MAX_THREADS;
|
|
}
|
|
|
|
static const char *CategoryName(int i) {
|
|
return (const char *)retro_atomic_load_acquire_ptr(&categories[i].name);
|
|
}
|
|
|
|
static bool SameCategory(const char *a, const char *b) {
|
|
#ifdef UNIFIED_CONST_STR
|
|
return a == b;
|
|
#else
|
|
return !strcmp(a, b);
|
|
#endif
|
|
}
|
|
|
|
int internal_profiler_find_cat(const char *category_name, bool create_missing) {
|
|
for (int i = 0; i < MAX_CATEGORIES; i++) {
|
|
const char *catname = CategoryName(i);
|
|
if (!catname) {
|
|
if (!category_name || !create_missing) {
|
|
return -1;
|
|
}
|
|
if (retro_atomic_cas_ptr(&categories[i].name, nullptr, (void *)category_name)) {
|
|
return i;
|
|
}
|
|
// Another thread took this slot first, maybe for the same name.
|
|
catname = CategoryName(i);
|
|
}
|
|
if (category_name && SameCategory(catname, category_name)) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Suspend, also used to prepare for leaving.
|
|
static void internal_profiler_suspend(int thread_id, int category, double now) {
|
|
double diff = now - profiler.eventStart[thread_id][category];
|
|
history[MAX_THREADS * profiler.historyPos + thread_id].time_taken[category] += (float)diff;
|
|
profiler.eventStart[thread_id][category] = 0.0;
|
|
}
|
|
|
|
// Resume, also used as part of entering.
|
|
static void internal_profiler_resume(int thread_id, int category, double now) {
|
|
profiler.eventStart[thread_id][category] = now;
|
|
}
|
|
|
|
int internal_profiler_enter(const char *category_name, int *out_thread_id) {
|
|
int category = internal_profiler_find_cat(category_name, true);
|
|
int thread_id = internal_profiler_find_thread();
|
|
if (category == -1 || !history) {
|
|
return category;
|
|
}
|
|
|
|
int &depth = profiler.depth[thread_id];
|
|
if (profiler.eventStart[thread_id][category] == 0.0f) {
|
|
double now = time_now_d();
|
|
int parent = profiler.parentCategory[thread_id][depth];
|
|
// Temporarily suspend the parent on entering a child.
|
|
if (parent != -1) {
|
|
internal_profiler_suspend(thread_id, parent, now);
|
|
}
|
|
internal_profiler_resume(thread_id, category, now);
|
|
} else {
|
|
DEBUG_LOG(Log::System, "profiler: recursive enter (%i - %s)", category, category_name);
|
|
}
|
|
|
|
depth++;
|
|
profiler.parentCategory[thread_id][depth] = category;
|
|
|
|
*out_thread_id = thread_id;
|
|
return category;
|
|
}
|
|
|
|
void internal_profiler_leave(int thread_id, int category) {
|
|
if (category == -1 || !history) {
|
|
return;
|
|
}
|
|
|
|
int &depth = profiler.depth[thread_id];
|
|
if (category < 0 || category >= MAX_CATEGORIES) {
|
|
ERROR_LOG(Log::System, "Bad category index %d", category);
|
|
depth--;
|
|
return;
|
|
}
|
|
|
|
double now = time_now_d();
|
|
|
|
depth--;
|
|
_assert_msg_(depth >= 0, "Profiler enter/leave mismatch!");
|
|
|
|
int parent = profiler.parentCategory[thread_id][depth];
|
|
// When there's recursion, we don't suspend or resume.
|
|
if (parent != category) {
|
|
internal_profiler_suspend(thread_id, category, now);
|
|
history[MAX_THREADS * profiler.historyPos + thread_id].count[category]++;
|
|
|
|
if (parent != -1) {
|
|
// Resume tracking the parent.
|
|
internal_profiler_resume(thread_id, parent, now);
|
|
}
|
|
}
|
|
}
|
|
|
|
void internal_profiler_end_frame() {
|
|
int thread_id = internal_profiler_find_thread();
|
|
_assert_msg_(profiler.depth[thread_id] == 0, "Can't be inside a profiler scope at end of frame!");
|
|
profiler.curFrameStart = time_now_d();
|
|
profiler.historyPos++;
|
|
profiler.historyPos &= (HISTORY_SIZE - 1);
|
|
memset(&history[MAX_THREADS * profiler.historyPos], 0, sizeof(CategoryFrame) * MAX_THREADS);
|
|
}
|
|
|
|
const char *Profiler_GetCategoryName(int i) {
|
|
return i >= 0 ? CategoryName(i) : "N/A";
|
|
}
|
|
|
|
int Profiler_GetHistoryLength() {
|
|
return HISTORY_SIZE;
|
|
}
|
|
|
|
int Profiler_GetNumCategories() {
|
|
for (int i = 0; i < MAX_CATEGORIES; i++) {
|
|
if (!CategoryName(i))
|
|
return i;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int Profiler_GetNumThreads() {
|
|
return NumThreads();
|
|
}
|
|
|
|
void Profiler_GetSlowestThreads(int *data, int count) {
|
|
int numCategories = Profiler_GetNumCategories();
|
|
for (int i = 0; i < HISTORY_SIZE; i++) {
|
|
int x = i - count + profiler.historyPos + 1;
|
|
while (x < 0)
|
|
x += HISTORY_SIZE;
|
|
while (x >= HISTORY_SIZE)
|
|
x -= HISTORY_SIZE;
|
|
|
|
float slowestTime = 0.0f;
|
|
data[i] = 0;
|
|
const int numThreads = NumThreads();
|
|
for (int thread = 0; thread < numThreads; ++thread) {
|
|
float sum = 0.0f;
|
|
for (int c = 0; c < numCategories; ++c) {
|
|
sum += history[MAX_THREADS * x + thread].time_taken[c];
|
|
}
|
|
if (sum > slowestTime) {
|
|
slowestTime = sum;
|
|
data[i] = thread;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Profiler_GetSlowestHistory(int category, int *slowestThreads, float *data, int count) {
|
|
for (int i = 0; i < HISTORY_SIZE; i++) {
|
|
int x = i - count + profiler.historyPos + 1;
|
|
while (x < 0)
|
|
x += HISTORY_SIZE;
|
|
while (x >= HISTORY_SIZE)
|
|
x -= HISTORY_SIZE;
|
|
|
|
int thread = slowestThreads[i];
|
|
data[i] = history[MAX_THREADS * x + thread].time_taken[category];
|
|
}
|
|
}
|
|
|
|
void Profiler_GetHistory(int category, int thread, float *data, int count) {
|
|
for (int i = 0; i < HISTORY_SIZE; i++) {
|
|
int x = i - count + profiler.historyPos + 1;
|
|
while (x < 0)
|
|
x += HISTORY_SIZE;
|
|
while (x >= HISTORY_SIZE)
|
|
x -= HISTORY_SIZE;
|
|
data[i] = history[MAX_THREADS * x + thread].time_taken[category];
|
|
}
|
|
}
|