From 0213194767396a5af2c1c7e163da351ec0c55617 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Tue, 3 Nov 2026 20:29:35 +0800 Subject: [PATCH 1/6] feat: multi-state sessions, portable CPU backend, independent CPU/mem - ProfilerSession per lua_State (map); parallel sessions across states - Separate cpuRunning/memRunning (no shared gRunning mutex) - POSIX ITIMER_PROF/SIGPROF walks all CPU sessions; LUA_MASKCOUNT fallback (default on Windows, optional via PLUA_CPU_BACKEND=count) - .pro v1 header (PLUA magic + version); write error checks; clear string tables on stop; fix llog year (tm_year+1900) - Configurable rates via set_config/get_config and env vars - Windows/POSIX IO isolation; luaopen_plua alias; require Lua >= 5.2 --- CMakeLists.txt | 29 +- src/CMakeLists.txt | 21 +- src/plua.cpp | 1211 +++++++++++++++++++++++++++----------------- 3 files changed, 792 insertions(+), 469 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b9b7c3..a3202b9 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,14 @@ project(pLua LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -g3 -DNDEBUG -O3") +if(MSVC) + add_compile_options(/W3 /DNDEBUG) + if(CMAKE_BUILD_TYPE STREQUAL "Release" OR NOT CMAKE_BUILD_TYPE) + add_compile_options(/O2) + endif() +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -g3 -DNDEBUG -O3") +endif() # Output directory for artifacts set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) @@ -14,6 +21,24 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) # Find Lua dependency find_package(Lua REQUIRED) +# Debian/Ubuntu often ship headers under include/lua5.x while also providing a +# dangling-ish /usr/include/lua.h symlink without sibling luaconf.h. Prefer the +# versioned include directory when present. +if(LUA_INCLUDE_DIR) + if(NOT EXISTS "${LUA_INCLUDE_DIR}/luaconf.h") + foreach(_ver 5.4 5.3 5.2) + if(EXISTS "${LUA_INCLUDE_DIR}/lua${_ver}/luaconf.h") + set(LUA_INCLUDE_DIR "${LUA_INCLUDE_DIR}/lua${_ver}") + break() + endif() + if(EXISTS "/usr/include/lua${_ver}/luaconf.h") + set(LUA_INCLUDE_DIR "/usr/include/lua${_ver}") + break() + endif() + endforeach() + endif() +endif() + # Look for lua CLI executable for testing find_program(LUA_EXECUTABLE NAMES lua lua5.4 lua5.3 lua5.2 lua5.1) @@ -23,7 +48,7 @@ add_subdirectory(src) find_program(GO_EXECUTABLE go) if(GO_EXECUTABLE) add_custom_target(tools_plua ALL - COMMAND ${GO_EXECUTABLE} build -o ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/plua plua.go + COMMAND ${GO_EXECUTABLE} build -o ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/plua${CMAKE_EXECUTABLE_SUFFIX} plua.go WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tools COMMENT "Building tools/plua via Go" ) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3f99b04..15807c5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,26 @@ add_library(plua SHARED plua.cpp) target_include_directories(plua PRIVATE ${LUA_INCLUDE_DIR}) + +# Produce libplua.so / libplua.dylib / libplua.dll so require("libplua") works +# consistently across platforms. On Windows this yields libplua.dll. set_target_properties(plua PROPERTIES PREFIX "lib" OUTPUT_NAME "plua") if(APPLE) - set_target_properties(plua PROPERTIES LINK_FLAGS "-undefined dynamic_lookup") + set_target_properties(plua PROPERTIES + LINK_FLAGS "-undefined dynamic_lookup" + SUFFIX ".so" # Lua on macOS commonly loads .so via package.cpath + ) +endif() + +if(WIN32) + # Export luaopen_* for require() + set_target_properties(plua PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) +endif() + +if(LUA_LIBRARIES) + # Usually not required for a Lua C module (symbols come from the host), + # but link when CMake provides them (helps some Windows toolchains). + if(WIN32) + target_link_libraries(plua PRIVATE ${LUA_LIBRARIES}) + endif() endif() diff --git a/src/plua.cpp b/src/plua.cpp index 41a2759..773a701 100755 --- a/src/plua.cpp +++ b/src/plua.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -7,64 +6,87 @@ #include #include #include -#include #include #include -#include -#include #include #include #include #include -#include -#include -#include #include #include -#include #include #include -#include #include #include +#if defined(_WIN32) +# define PLUA_WINDOWS 1 +# ifndef _SSIZE_T_DEFINED +# include + typedef SSIZE_T ssize_t; +# define _SSIZE_T_DEFINED +# endif +# include +# include +# include +# include +# define open _open +# define close _close +# define write _write +# define O_CREAT _O_CREAT +# define O_WRONLY _O_WRONLY +# define O_TRUNC _O_TRUNC +# ifndef S_IRUSR +# define S_IRUSR _S_IREAD +# define S_IWUSR _S_IWRITE +# endif +#else +# include +# include +# include +# include +# define PLUA_HAS_POSIX_PROF 1 +#endif + extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" } +#if LUA_VERSION_NUM < 502 +# error "pLua requires Lua 5.2 or newer (Lua 5.1 / LuaJIT are not supported by this build)" +#endif + const int open_debug = 0; -int gSampleCount = 0; -std::string gFilename; -lua_State *gL = 0; -int gRunning = 0; #define LLOG(...) if (open_debug) {llog("[DEBUG] ", __FILE__, __FUNCTION__, __LINE__, __VA_ARGS__);} #define LERR(...) if (open_debug) {llog("[ERROR] ", __FILE__, __FUNCTION__, __LINE__, __VA_ARGS__);} -void llog(const char *header, const char *file, const char *func, int pos, const char *fmt, ...) { - FILE *pLog = NULL; - time_t clock1; - struct tm *tptr; - va_list ap; - - pLog = fopen("plua.log", "a+"); +static void llog(const char *header, const char *file, const char *func, int pos, const char *fmt, ...) { + FILE *pLog = fopen("plua.log", "a+"); if (pLog == NULL) { return; } - clock1 = time(0); - tptr = localtime(&clock1); + time_t clock1 = time(0); + struct tm *tptr = localtime(&clock1); + long long ms = 0; +#if defined(PLUA_WINDOWS) + ms = (long long)clock1 * 1000; +#else struct timeval tv; gettimeofday(&tv, NULL); + ms = (long long)((tv.tv_sec) * 1000 + (tv.tv_usec) / 1000); +#endif - fprintf(pLog, "===========================[%d.%d.%d, %d.%d.%d %llu]%s:%d,%s:===========================\n%s", - tptr->tm_year + 1990, tptr->tm_mon + 1, + fprintf(pLog, "===========================[%d.%d.%d, %d.%d.%d %lld]%s:%d,%s:===========================\n%s", + tptr->tm_year + 1900, tptr->tm_mon + 1, tptr->tm_mday, tptr->tm_hour, tptr->tm_min, - tptr->tm_sec, (long long) ((tv.tv_sec) * 1000 + (tv.tv_usec) / 1000), file, pos, func, header); + tptr->tm_sec, ms, file, pos, func, header); + va_list ap; va_start(ap, fmt); vfprintf(pLog, fmt, ap); fprintf(pLog, "\n\n"); @@ -79,16 +101,22 @@ void llog(const char *header, const char *file, const char *func, int pos, const } static const int MAX_FUNC_NAME_SIZE = 127; +static const int MAX_STACK_SIZE = 64; +static const int DEFAULT_CPU_SAMPLE_ITER_MS = 10; +static const int DEFAULT_CPU_HOOK_COUNT = 1000; +static const int DEFAULT_MEM_PROFILE_RATE = 524288; // 512K +static const uint32_t PLUA_PRO_MAGIC = 0x41554C50; // 'PLUA' LE +static const uint32_t PLUA_PRO_VERSION = 1; static std::string get_funcname(lua_State *L, lua_Debug *ar) { char buf[MAX_FUNC_NAME_SIZE + 1] = {0}; - if (*ar->namewhat != '\0') /* is there a name from code? */ { - snprintf(buf, MAX_FUNC_NAME_SIZE, "%s '%s'", ar->namewhat, ar->name); /* use it */ - } else if (*ar->what == 'm') /* main? */ { + if (*ar->namewhat != '\0') { + snprintf(buf, MAX_FUNC_NAME_SIZE, "%s '%s'", ar->namewhat, ar->name); + } else if (*ar->what == 'm') { snprintf(buf, MAX_FUNC_NAME_SIZE, "main chunk"); - } else if (*ar->what != 'C') /* for Lua functions, use */ { + } else if (*ar->what != 'C') { snprintf(buf, MAX_FUNC_NAME_SIZE, "function <%s:%d>", ar->short_src, ar->linedefined); - } else /* nothing left... */ { + } else { snprintf(buf, MAX_FUNC_NAME_SIZE, "%s(%d): %s\n", ar->short_src, ar->currentline, ar->name ? ar->name : "?"); } return buf; @@ -97,12 +125,10 @@ static std::string get_funcname(lua_State *L, lua_Debug *ar) { static int lastlevel(lua_State *L) { lua_Debug ar; int li = 1, le = 1; - /* find the bottom index of the call stack. */ while (lua_getstack(L, le, &ar)) { li = le; le *= 2; } - /* do a binary search */ while (li < le) { int m = (li + le) / 2; if (lua_getstack(L, m, &ar)) li = m + 1; @@ -111,18 +137,11 @@ static int lastlevel(lua_State *L) { return le - 1; } -std::unordered_map gString2Id; -std::unordered_map gId2String; - static const char *IGNORE_NAME[] = {"?", "function 'xpcall'", "upvalue 'xpcall'", "field 'xpcall'", "function 'pcall'", "upvalue 'pcall'", "field 'pcall'", "function", "local 'func'"}; static const int VALID_MIN_ID = sizeof(IGNORE_NAME) / sizeof(const char *); -static const int MAX_STACK_SIZE = 64; - -static const int CPU_SAMPLE_ITER = 10; - struct CallStack { int depth; int stack[MAX_STACK_SIZE]; @@ -152,99 +171,176 @@ struct CallStackEqual { struct ProfileData { std::unordered_map callstack; int total = 0; - int fd = 0; + int fd = -1; }; -ProfileData gProfileData; - -static void flush_file(int fd, const char *buf, size_t len) { - while (len > 0) { - ssize_t r = write(fd, buf, len); - buf += r; - len -= r; - } -} +struct MemInfo { + MemInfo() {} + MemInfo(uint32_t a, uint32_t f) : allocs(a), frees(f) {} + uint32_t allocs = 0; + uint32_t frees = 0; +}; -static void flush() { - if (gProfileData.total <= 0) { - return; +struct CallStackPointerHash { + size_t operator()(CallStack *cs) const { + size_t hash = 0; + for (int i = 0; i < cs->depth; i++) { + int id = cs->stack[i]; + hash = (hash << 8) | (hash >> (8 * (sizeof(hash) - 1))); + hash += (id * 31) + (id * 7) + (id * 3); + } + return hash; } +}; - LLOG("flush..."); - - for (auto iter = gProfileData.callstack.begin(); iter != gProfileData.callstack.end(); iter++) { - const CallStack &cs = iter->first; - int count = iter->second; - - flush_file(gProfileData.fd, (const char *) &count, sizeof(count)); - flush_file(gProfileData.fd, (const char *) &cs, sizeof(cs)); +struct CallStackPointerEqual { + bool operator()(CallStack *cs1, CallStack *cs2) const { + if (cs1->depth != cs2->depth) { + return false; + } + return memcmp(cs1->stack, cs2->stack, sizeof(int) * cs1->depth) == 0; } +}; - int total_len = 0; - for (auto iter = gString2Id.begin(); iter != gString2Id.end(); iter++) { - const std::string &str = iter->first; - int id = iter->second; +struct MemProfileData { + std::unordered_map callstack; + std::unordered_map ptr2Callstack; + int total = 0; + lua_Alloc oldAlloc = NULL; + void *oldAllocUd = NULL; + ssize_t nextSample = 0; + uint64_t rand = 0; + int alloc_size_fd = -1; + int usage_fd = -1; + int is_in_hook = 0; + void *hook_alloc_ptr = 0; + size_t hook_alloc_sz = 0; +}; - if (id < VALID_MIN_ID) { - continue; - } +struct ProfilerSession { + lua_State *L = NULL; + bool cpuRunning = false; + bool memRunning = false; + bool useSigprof = false; - int len = str.length(); - len = len > MAX_FUNC_NAME_SIZE ? MAX_FUNC_NAME_SIZE : len; - flush_file(gProfileData.fd, str.c_str(), len); - flush_file(gProfileData.fd, (const char *) &len, sizeof(len)); + int cpuSampleLimit = 0; // 0 = unlimited (from duration seconds) + int cpuIntervalMs = DEFAULT_CPU_SAMPLE_ITER_MS; + int cpuHookCount = DEFAULT_CPU_HOOK_COUNT; + std::string cpuFilename; + ProfileData cpuData; - flush_file(gProfileData.fd, (const char *) &id, sizeof(id)); - total_len++; - } + int memSampleLimit = 0; + int memProfileRate = DEFAULT_MEM_PROFILE_RATE; + std::string memFilename; + MemProfileData memData; - flush_file(gProfileData.fd, (const char *) &total_len, sizeof(total_len)); + std::unordered_map string2Id; + std::unordered_map id2String; - int total = gProfileData.total; - LLOG("flush ok %d", gProfileData.total); + bool pendingCpuSample = false; + bool pendingMemSample = false; +}; - gProfileData.total = 0; - gProfileData.callstack.clear(); +static std::map gSessions; +#if defined(PLUA_HAS_POSIX_PROF) +static int gPosixTimerUsers = 0; +static bool gSigHandlerInstalled = false; +#endif + +// Global defaults (overridable via env / Lua API before start) +static int gDefaultCpuIntervalMs = DEFAULT_CPU_SAMPLE_ITER_MS; +static int gDefaultCpuHookCount = DEFAULT_CPU_HOOK_COUNT; +static int gDefaultMemProfileRate = DEFAULT_MEM_PROFILE_RATE; +static int gForceCountBackend = 0; // 1 = force LUA_MASKCOUNT + +static int env_int(const char *name, int defval) { + const char *v = getenv(name); + if (!v || !*v) return defval; + char *end = NULL; + long n = strtol(v, &end, 10); + if (end == v) return defval; + return (int)n; +} - if (gProfileData.fd != 0) { - close(gProfileData.fd); - gProfileData.fd = 0; +static void load_env_defaults() { + static int loaded = 0; + if (loaded) return; + loaded = 1; + gDefaultCpuIntervalMs = env_int("PLUA_CPU_INTERVAL_MS", DEFAULT_CPU_SAMPLE_ITER_MS); + if (gDefaultCpuIntervalMs <= 0) gDefaultCpuIntervalMs = DEFAULT_CPU_SAMPLE_ITER_MS; + gDefaultCpuHookCount = env_int("PLUA_CPU_HOOK_COUNT", DEFAULT_CPU_HOOK_COUNT); + if (gDefaultCpuHookCount <= 0) gDefaultCpuHookCount = DEFAULT_CPU_HOOK_COUNT; + gDefaultMemProfileRate = env_int("PLUA_MEM_RATE", DEFAULT_MEM_PROFILE_RATE); + if (gDefaultMemProfileRate <= 0) gDefaultMemProfileRate = DEFAULT_MEM_PROFILE_RATE; + const char *backend = getenv("PLUA_CPU_BACKEND"); + if (backend && (strcmp(backend, "count") == 0 || strcmp(backend, "maskcount") == 0)) { + gForceCountBackend = 1; } - - printf("pLua flush ok %d\n", total); +#if !defined(PLUA_HAS_POSIX_PROF) + gForceCountBackend = 1; +#endif } -static int lrealstopsafe(lua_State *L) { - gRunning = 0; - - struct itimerval timer; - timer.it_interval.tv_sec = 0; - timer.it_interval.tv_usec = 0; - timer.it_value = timer.it_interval; - int ret = setitimer(ITIMER_PROF, &timer, NULL); - if (ret != 0) { - LERR("setitimer fail %d", ret); - return ret; +static ProfilerSession *get_session(lua_State *L, bool create) { + auto it = gSessions.find(L); + if (it != gSessions.end()) { + return it->second; } + if (!create) return NULL; + ProfilerSession *s = new ProfilerSession(); + s->L = L; + load_env_defaults(); + s->cpuIntervalMs = gDefaultCpuIntervalMs; + s->cpuHookCount = gDefaultCpuHookCount; + s->memProfileRate = gDefaultMemProfileRate; + gSessions[L] = s; + return s; +} - flush(); +static void clear_string_tables(ProfilerSession *s) { + s->string2Id.clear(); + s->id2String.clear(); +} - lua_sethook(L, 0, 0, 0); +static void init_ignore_names(ProfilerSession *s) { + for (int i = 0; i < VALID_MIN_ID; i++) { + s->string2Id[IGNORE_NAME[i]] = i; + s->id2String[i] = IGNORE_NAME[i]; + } +} - return 0; +static bool flush_file(int fd, const char *buf, size_t len) { + if (fd < 0) return false; + while (len > 0) { + int r = (int)write(fd, buf, (unsigned int)len); + if (r < 0) { + if (errno == EINTR) continue; + LERR("write fail errno=%d", errno); + return false; + } + if (r == 0) { + LERR("write returned 0"); + return false; + } + buf += r; + len -= (size_t)r; + } + return true; } -extern "C" int lrealstop(lua_State *L) { - gRunning = 0; - return 0; +static bool write_pro_header(int fd) { + uint32_t magic = PLUA_PRO_MAGIC; + uint32_t version = PLUA_PRO_VERSION; + if (!flush_file(fd, (const char *)&magic, sizeof(magic))) return false; + if (!flush_file(fd, (const char *)&version, sizeof(version))) return false; + return true; } -static void get_cur_callstack(lua_State *L, CallStack &cs) { +static void get_cur_callstack(ProfilerSession *s, CallStack &cs) { + lua_State *L = s->L; lua_Debug ar; - int last = lastlevel(L); int i = 0; - cs.depth = 0; while (lua_getstack(L, last, &ar) && i < MAX_STACK_SIZE) { @@ -255,11 +351,11 @@ static void get_cur_callstack(lua_State *L, CallStack &cs) { last--; int id = 0; - auto iter = gString2Id.find(funcname); - if (iter == gString2Id.end()) { - id = gString2Id.size(); - gString2Id[funcname] = id; - gId2String[id] = funcname; + auto iter = s->string2Id.find(funcname); + if (iter == s->string2Id.end()) { + id = (int)s->string2Id.size(); + s->string2Id[funcname] = id; + s->id2String[id] = funcname; } else { id = iter->second; } @@ -268,174 +364,422 @@ static void get_cur_callstack(lua_State *L, CallStack &cs) { continue; } - LLOG("%s %d %d", funcname, id, last); - + LLOG("%s %d %d", funcname.c_str(), id, last); cs.stack[cs.depth] = id; cs.depth++; } } -static void SignalHandlerHook(lua_State *L, lua_Debug *par) { - lua_sethook(L, 0, 0, 0); +static bool flush_string_table(ProfilerSession *s, int fd) { + int total_len = 0; + for (auto iter = s->string2Id.begin(); iter != s->string2Id.end(); iter++) { + const std::string &str = iter->first; + int id = iter->second; + if (id < VALID_MIN_ID) continue; - LLOG("Hook..."); + int len = (int)str.length(); + len = len > MAX_FUNC_NAME_SIZE ? MAX_FUNC_NAME_SIZE : len; + if (!flush_file(fd, str.c_str(), (size_t)len)) return false; + if (!flush_file(fd, (const char *)&len, sizeof(len))) return false; + if (!flush_file(fd, (const char *)&id, sizeof(id))) return false; + total_len++; + } + if (!flush_file(fd, (const char *)&total_len, sizeof(total_len))) return false; + return true; +} + +static void flush_cpu(ProfilerSession *s) { + if (s->cpuData.total <= 0) { + if (s->cpuData.fd >= 0) { + close(s->cpuData.fd); + s->cpuData.fd = -1; + } + return; + } + + LLOG("flush cpu..."); + bool ok = true; + for (auto iter = s->cpuData.callstack.begin(); iter != s->cpuData.callstack.end(); iter++) { + const CallStack &cs = iter->first; + int count = iter->second; + if (!flush_file(s->cpuData.fd, (const char *)&count, sizeof(count))) { ok = false; break; } + if (!flush_file(s->cpuData.fd, (const char *)&cs, sizeof(cs))) { ok = false; break; } + } + if (ok) { + ok = flush_string_table(s, s->cpuData.fd); + } + + int total = s->cpuData.total; + s->cpuData.total = 0; + s->cpuData.callstack.clear(); + + if (s->cpuData.fd >= 0) { + close(s->cpuData.fd); + s->cpuData.fd = -1; + } + + if (ok) { + printf("pLua flush ok %d\n", total); + } else { + printf("pLua flush failed (write error), samples=%d\n", total); + } +} + +static std::string get_mem_filename(std::string path, const std::string &suffix) { + auto pos = path.find_last_of('.'); + if (pos == std::string::npos) { + return path + suffix; + } + path.insert(pos, suffix); + return path; +} + +static void flush_mem_left(ProfilerSession *s, int fd) { + flush_string_table(s, fd); + if (fd >= 0) { + close(fd); + } +} + +static void flush_mem_alloc_size(ProfilerSession *s) { + int fd = s->memData.alloc_size_fd; + for (auto iter = s->memData.callstack.begin(); iter != s->memData.callstack.end(); iter++) { + const CallStack &cs = *iter->first; + auto &mem_info = iter->second; + flush_file(fd, (const char *)&mem_info.allocs, sizeof(mem_info.allocs)); + flush_file(fd, (const char *)&cs, sizeof(cs)); + } + flush_mem_left(s, fd); + s->memData.alloc_size_fd = -1; +} + +static void flush_mem_usage(ProfilerSession *s) { + int fd = s->memData.usage_fd; + for (auto iter = s->memData.callstack.begin(); iter != s->memData.callstack.end(); iter++) { + const CallStack &cs = *iter->first; + auto &mem_info = iter->second; + auto usage = (int)mem_info.allocs - (int)mem_info.frees; + if (usage <= 0) continue; + flush_file(fd, (const char *)&usage, sizeof(usage)); + flush_file(fd, (const char *)&cs, sizeof(cs)); + } + flush_mem_left(s, fd); + s->memData.usage_fd = -1; +} - if (gRunning == 0 || (gSampleCount != 0 && gSampleCount <= gProfileData.total)) { - LLOG("lrealstop..."); - lrealstopsafe(L); +static void flush_mem(ProfilerSession *s) { + if (s->memData.total <= 0) { + if (s->memData.alloc_size_fd >= 0) { close(s->memData.alloc_size_fd); s->memData.alloc_size_fd = -1; } + if (s->memData.usage_fd >= 0) { close(s->memData.usage_fd); s->memData.usage_fd = -1; } return; } + LLOG("flush mem..."); + flush_mem_alloc_size(s); + flush_mem_usage(s); + + int total = s->memData.total; + s->memData.total = 0; + for (auto iter = s->memData.callstack.begin(); iter != s->memData.callstack.end(); iter++) { + delete iter->first; + } + s->memData.callstack.clear(); + s->memData.ptr2Callstack.clear(); + printf("pLua flush ok %d\n", total); +} + +static void rearm_session_hook(ProfilerSession *s); + +static void take_cpu_sample(ProfilerSession *s) { + if (!s->cpuRunning) return; + if (s->cpuSampleLimit != 0 && s->cpuSampleLimit <= s->cpuData.total) { + return; + } CallStack cs; - get_cur_callstack(L, cs); + get_cur_callstack(s, cs); + s->cpuData.callstack[cs]++; + s->cpuData.total++; +} + +static void take_mem_sample(ProfilerSession *s) { + s->memData.is_in_hook++; + auto hook_alloc_ptr = s->memData.hook_alloc_ptr; + auto hook_alloc_sz = s->memData.hook_alloc_sz; + + CallStack cs; + get_cur_callstack(s, cs); - gProfileData.callstack[cs]++; - gProfileData.total++; + auto it = s->memData.callstack.find(&cs); + CallStack *pointer_cs = 0; + if (it == s->memData.callstack.end()) { + auto new_cs = new CallStack(); + memcpy(new_cs, &cs, sizeof(cs)); + s->memData.callstack[new_cs] = MemInfo(1, 0); + pointer_cs = new_cs; + } else { + it->second.allocs++; + pointer_cs = it->first; + } + s->memData.ptr2Callstack[hook_alloc_ptr] = pointer_cs; + LLOG("alloc %p %u", hook_alloc_ptr, (unsigned)hook_alloc_sz); + s->memData.is_in_hook--; } -static void SignalHandler(int sig, siginfo_t *sinfo, void *ucontext) { - lua_sethook(gL, SignalHandlerHook, LUA_MASKCOUNT, 1); +static void SessionHook(lua_State *L, lua_Debug *par) { + ProfilerSession *s = get_session(L, false); + if (!s) { + lua_sethook(L, 0, 0, 0); + return; + } + + bool didCpu = s->pendingCpuSample; + bool didMem = s->pendingMemSample; + s->pendingCpuSample = false; + s->pendingMemSample = false; + + if (didCpu) { + if (s->cpuRunning && (s->cpuSampleLimit == 0 || s->cpuSampleLimit > s->cpuData.total)) { + take_cpu_sample(s); + } + if (s->cpuRunning && s->cpuSampleLimit != 0 && s->cpuSampleLimit <= s->cpuData.total) { + // stop cpu when limit hit — defer actual stop to avoid reentrancy issues + // Mark and stop safely below after mem + s->cpuRunning = false; +#if defined(PLUA_HAS_POSIX_PROF) + if (s->useSigprof && gPosixTimerUsers > 0) { + gPosixTimerUsers--; + if (gPosixTimerUsers == 0) { + struct itimerval timer; + memset(&timer, 0, sizeof(timer)); + setitimer(ITIMER_PROF, &timer, NULL); + } + } +#endif + flush_cpu(s); + if (!s->memRunning) { + clear_string_tables(s); + } + } + } + if (didMem) { + take_mem_sample(s); + } + + rearm_session_hook(s); } -static int lrealstartsafe(lua_State *L) { - if (gRunning) { - LERR("start again, failed"); - return -1; +static void rearm_session_hook(ProfilerSession *s) { + if (!s) return; + if (s->pendingCpuSample || s->pendingMemSample) { + lua_sethook(s->L, SessionHook, LUA_MASKCOUNT, 1); + return; } - gRunning = 1; + // Count-backend continuous sampling + if (s->cpuRunning && !s->useSigprof) { + lua_sethook(s->L, SessionHook, LUA_MASKCOUNT, s->cpuHookCount); + s->pendingCpuSample = true; // next count fires a sample + return; + } + lua_sethook(s->L, 0, 0, 0); +} - for (int i = 0; i < VALID_MIN_ID; i++) { - gString2Id[IGNORE_NAME[i]] = i; - gId2String[i] = IGNORE_NAME[i]; +static void request_hook(ProfilerSession *s, bool cpu, bool mem) { + if (cpu) s->pendingCpuSample = true; + if (mem) s->pendingMemSample = true; + lua_sethook(s->L, SessionHook, LUA_MASKCOUNT, 1); +} + +#if defined(PLUA_HAS_POSIX_PROF) +static void SignalHandler(int sig, siginfo_t *sinfo, void *ucontext) { + (void)sig; (void)sinfo; (void)ucontext; + for (auto &kv : gSessions) { + ProfilerSession *s = kv.second; + if (s->cpuRunning && s->useSigprof) { + request_hook(s, true, false); + } } +} +static int ensure_sigprof_handler() { + if (gSigHandlerInstalled) return 0; struct sigaction sa; + memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = SignalHandler; sa.sa_flags = SA_RESTART | SA_SIGINFO; sigemptyset(&sa.sa_mask); - if (sigaction(SIGPROF, &sa, NULL) == -1) { - LERR("sigaction(SIGALRM) failed"); + LERR("sigaction(SIGPROF) failed"); return -1; } + gSigHandlerInstalled = true; + return 0; +} - int fd = open(gFilename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); - if (fd < 0) { - LERR("open file fail %s", gFilename.c_str()); - return -1; +static int start_posix_timer(ProfilerSession *s) { + if (ensure_sigprof_handler() != 0) return -1; + if (gPosixTimerUsers == 0) { + struct itimerval timer; + timer.it_interval.tv_sec = 0; + timer.it_interval.tv_usec = s->cpuIntervalMs * 1000; + timer.it_value = timer.it_interval; + int ret = setitimer(ITIMER_PROF, &timer, NULL); + if (ret != 0) { + LERR("setitimer fail %d", ret); + return -1; + } + } + gPosixTimerUsers++; + return 0; +} + +static void stop_posix_timer(ProfilerSession *s) { + (void)s; + if (gPosixTimerUsers <= 0) return; + gPosixTimerUsers--; + if (gPosixTimerUsers == 0) { + struct itimerval timer; + memset(&timer, 0, sizeof(timer)); + setitimer(ITIMER_PROF, &timer, NULL); + } +} +#endif + +static int start_cpu_backend(ProfilerSession *s) { +#if defined(PLUA_HAS_POSIX_PROF) + if (!gForceCountBackend) { + s->useSigprof = true; + return start_posix_timer(s); + } +#endif + s->useSigprof = false; + s->pendingCpuSample = false; + // SessionHook / rearm_session_hook arms LUA_MASKCOUNT sampling. + return 0; +} + +static void stop_cpu_backend(ProfilerSession *s) { +#if defined(PLUA_HAS_POSIX_PROF) + if (s->useSigprof) { + stop_posix_timer(s); + } +#endif + s->useSigprof = false; + // Only clear hook if mem is not pending work; rearm handles it + rearm_session_hook(s); +} + +static int lrealstopsafe(lua_State *L) { + ProfilerSession *s = get_session(L, false); + if (!s) { + return 0; + } + // lrealstop may have cleared cpuRunning already (async stop); still tear down. + bool was_running = s->cpuRunning || s->cpuData.fd >= 0 || s->cpuData.total > 0; + if (!was_running) { + return 0; + } + s->cpuRunning = false; + stop_cpu_backend(s); + flush_cpu(s); + if (!s->memRunning) { + clear_string_tables(s); } + return 0; +} - gProfileData.fd = fd; +extern "C" int lrealstop(lua_State *L) { + ProfilerSession *s = get_session(L, false); + if (s) s->cpuRunning = false; + return 0; +} - struct itimerval timer; - timer.it_interval.tv_sec = 0; - timer.it_interval.tv_usec = CPU_SAMPLE_ITER * 1000; - timer.it_value = timer.it_interval; - int ret = setitimer(ITIMER_PROF, &timer, NULL); - if (ret != 0) { - LERR("setitimer fail %d", ret); +static int lrealstartsafe(lua_State *L) { + ProfilerSession *s = get_session(L, false); + if (!s) return -1; + if (s->cpuRunning) { + LERR("cpu start again, failed"); return -1; } - gProfileData.total = 0; - gProfileData.callstack.clear(); + if (s->string2Id.empty()) { + init_ignore_names(s); + } + + int fd = open(s->cpuFilename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); + if (fd < 0) { + LERR("open file fail %s", s->cpuFilename.c_str()); + return -1; + } + if (!write_pro_header(fd)) { + close(fd); + LERR("write header fail %s", s->cpuFilename.c_str()); + return -1; + } + s->cpuData.fd = fd; + s->cpuData.total = 0; + s->cpuData.callstack.clear(); + if (start_cpu_backend(s) != 0) { + close(fd); + s->cpuData.fd = -1; + return -1; + } + s->cpuRunning = true; return 0; } static void StartHandlerHook(lua_State *L, lua_Debug *par) { + (void)par; lrealstartsafe(L); - lua_sethook(L, 0, 0, 0); + ProfilerSession *s = get_session(L, false); + rearm_session_hook(s); } extern "C" int lrealstart(lua_State *L, int second, const char *file) { - if (gRunning) { - LERR("start again, failed"); + load_env_defaults(); + ProfilerSession *s = get_session(L, true); + if (s->cpuRunning) { + LERR("cpu start again, failed"); + return -1; + } + if (!file) { + LERR("cpu start missing file"); return -1; } - gL = L; - gSampleCount = second * 1000 / CPU_SAMPLE_ITER; - gFilename = file; - - // lrealstart可能被注入调用,在StartHandlerHook里面执行具体的逻辑 - lua_sethook(gL, StartHandlerHook, LUA_MASKCOUNT, 1); - - LLOG("lstart %u %s", gSampleCount, file); + s->cpuIntervalMs = gDefaultCpuIntervalMs; + s->cpuHookCount = gDefaultCpuHookCount; + s->cpuSampleLimit = (second > 0) ? (second * 1000 / s->cpuIntervalMs) : 0; + s->cpuFilename = file; + lua_sethook(L, StartHandlerHook, LUA_MASKCOUNT, 1); + LLOG("lstart %u %s", s->cpuSampleLimit, file); return 0; } static int lstart(lua_State *L) { - int second = (int) lua_tointeger(L, 1); + int second = (int)lua_tointeger(L, 1); const char *file = lua_tostring(L, 2); + if (lua_gettop(L) >= 3 && lua_isnumber(L, 3)) { + int ms = (int)lua_tointeger(L, 3); + if (ms > 0) gDefaultCpuIntervalMs = ms; + } int ret = lrealstart(L, second, file); lua_pushinteger(L, ret); return 1; } static int lstop(lua_State *L) { - LLOG("lstop %s", gFilename.c_str()); + ProfilerSession *s = get_session(L, false); + LLOG("lstop %s", s ? s->cpuFilename.c_str() : ""); int ret = lrealstop(L); lrealstopsafe(L); lua_pushinteger(L, ret); return 1; } -//////////////////////////////////mem profiler start//////////////////////////////////////// - -static const int MEM_PROFILE_RATE = 524288; // 512K - -struct MemInfo { - MemInfo() {} - - MemInfo(uint32_t a, uint32_t f) { - allocs = a; - frees = f; - } - - uint32_t allocs = 0; - uint32_t frees = 0; -}; - -struct CallStackPointerHash { - size_t operator()(CallStack *cs) const { - size_t hash = 0; - for (int i = 0; i < cs->depth; i++) { - int id = cs->stack[i]; - hash = (hash << 8) | (hash >> (8 * (sizeof(hash) - 1))); - hash += (id * 31) + (id * 7) + (id * 3); - } - return hash; - } -}; - -struct CallStackPointerEqual { - bool operator()(CallStack *cs1, CallStack *cs2) const { - if (cs1->depth != cs2->depth) { - return false; - } - return memcmp(cs1->stack, cs2->stack, sizeof(int) * cs1->depth) == 0; - } -}; - -struct MemProfileData { - std::unordered_map callstack; - std::unordered_map ptr2Callstack; - int total = 0; - lua_Alloc oldAlloc = NULL; - ssize_t nextSample = 0; - uint64_t rand = 0; - int alloc_size_fd = 0; - int usage_fd = 0; - int is_in_hook = 0; - void *hook_alloc_ptr = 0; - size_t hook_alloc_sz = 0; -}; - -MemProfileData gMemProfileData; +//////////////////////////////////mem profiler//////////////////////////////////////// -// 移植自gperftools的Sampler::NextRandom static uint64_t next_random(uint64_t rnd) { const uint64_t prng_mult = 0x5DEECE66DULL; const uint64_t prng_add = 0xB; @@ -446,363 +790,298 @@ static uint64_t next_random(uint64_t rnd) { #define MAX_SSIZE (static_cast(static_cast(static_cast(-1)) >> 1)) -// 移植自gperftools的Sampler::PickNextSamplingPoint -static ssize_t gen_next_sample() { - gMemProfileData.rand = next_random(gMemProfileData.rand); - // Take the top 26 bits as the random number - // (This plus the 1<<58 sampling bound give a max possible step of - // 5194297183973780480 bytes.) - const uint64_t prng_mod_power = 48; // Number of bits in prng - // The uint32_t cast is to prevent a (hard-to-reproduce) NAN - // under piii debug for some binaries. - double q = static_cast(gMemProfileData.rand >> (prng_mod_power - 26)) + 1.0; - // Put the computed p-value through the CDF of a geometric. - double interval = (log2(q) - 26) * (-log(2.0) * MEM_PROFILE_RATE); - - // Very large values of interval overflow ssize_t. If we happen to - // hit such improbable condition, we simply cheat and clamp interval - // to largest supported value. +static ssize_t gen_next_sample(ProfilerSession *s) { + s->memData.rand = next_random(s->memData.rand); + const uint64_t prng_mod_power = 48; + double q = static_cast(s->memData.rand >> (prng_mod_power - 26)) + 1.0; + double interval = (log2(q) - 26) * (-log(2.0) * s->memProfileRate); return static_cast(std::min(interval, static_cast(MAX_SSIZE))); } -static void flush_mem_left(int fd) { - int total_len = 0; - for (auto iter = gString2Id.begin(); iter != gString2Id.end(); iter++) { - const std::string &str = iter->first; - int id = iter->second; - - if (id < VALID_MIN_ID) { - continue; - } - - int len = str.length(); - len = len > MAX_FUNC_NAME_SIZE ? MAX_FUNC_NAME_SIZE : len; - flush_file(fd, str.c_str(), len); - flush_file(fd, (const char *) &len, sizeof(len)); - - flush_file(fd, (const char *) &id, sizeof(id)); - total_len++; - } - - flush_file(fd, (const char *) &total_len, sizeof(total_len)); - - if (fd != 0) { - close(fd); - } -} - -static void flush_mem_alloc_size() { - int fd = gMemProfileData.alloc_size_fd; - - for (auto iter = gMemProfileData.callstack.begin(); iter != gMemProfileData.callstack.end(); iter++) { - const CallStack &cs = *iter->first; - auto &mem_info = iter->second; - - flush_file(fd, (const char *) &mem_info.allocs, sizeof(mem_info.allocs)); - flush_file(fd, (const char *) &cs, sizeof(cs)); +static int lrealstopmemsafe(lua_State *L) { + ProfilerSession *s = get_session(L, false); + if (!s) { + return 0; } - - flush_mem_left(fd); - gMemProfileData.alloc_size_fd = 0; -} - -static void flush_mem_usage() { - int fd = gMemProfileData.usage_fd; - - for (auto iter = gMemProfileData.callstack.begin(); iter != gMemProfileData.callstack.end(); iter++) { - const CallStack &cs = *iter->first; - auto &mem_info = iter->second; - auto usage = (int) mem_info.allocs - (int) mem_info.frees; - if (usage <= 0) { - continue; - } - - flush_file(fd, (const char *) &usage, sizeof(usage)); - flush_file(fd, (const char *) &cs, sizeof(cs)); + bool was_running = s->memRunning || s->memData.oldAlloc != NULL || + s->memData.alloc_size_fd >= 0 || s->memData.usage_fd >= 0; + if (!was_running) { + return 0; } - flush_mem_left(fd); - gMemProfileData.usage_fd = 0; -} - -static void flush_mem() { - if (gMemProfileData.total <= 0) { - return; + if (s->memData.oldAlloc) { + lua_setallocf(L, s->memData.oldAlloc, s->memData.oldAllocUd); + s->memData.oldAlloc = NULL; + s->memData.oldAllocUd = NULL; } - LLOG("flush..."); - - flush_mem_alloc_size(); - flush_mem_usage(); - - int total = gMemProfileData.total; - LLOG("flush ok %d", gMemProfileData.total); - - gMemProfileData.total = 0; - for (auto iter = gMemProfileData.callstack.begin(); iter != gMemProfileData.callstack.end(); iter++) { - delete iter->first; + s->memRunning = false; + flush_mem(s); + if (!s->cpuRunning) { + clear_string_tables(s); + rearm_session_hook(s); } - gMemProfileData.callstack.clear(); - gMemProfileData.ptr2Callstack.clear(); - - printf("pLua flush ok %d\n", total); -} - -static int lrealstopmemsafe(lua_State *L) { - - lua_setallocf(L, gMemProfileData.oldAlloc, NULL); - - gRunning = 0; - - flush_mem(); - - lua_sethook(L, 0, 0, 0); - return 0; } extern "C" int lrealstopmem(lua_State *L) { - gRunning = 0; + ProfilerSession *s = get_session(L, false); + if (s) s->memRunning = false; return 0; } -static void my_lua_Alloc_safe() { - gMemProfileData.is_in_hook++; - - auto hook_alloc_ptr = gMemProfileData.hook_alloc_ptr; - auto hook_alloc_sz = gMemProfileData.hook_alloc_sz; - - CallStack cs; - get_cur_callstack(gL, cs); // 内部可能触发再次分配内存 - - auto it = gMemProfileData.callstack.find(&cs); - CallStack *pointer_cs = 0; - if (it == gMemProfileData.callstack.end()) { - auto new_cs = new CallStack(); - memcpy(new_cs, &cs, sizeof(cs)); - gMemProfileData.callstack[new_cs] = MemInfo(1, 0); - pointer_cs = new_cs; - } else { - auto &mem_info = it->second; - mem_info.allocs++; - pointer_cs = it->first; - } - - // add new pointer - gMemProfileData.ptr2Callstack[hook_alloc_ptr] = pointer_cs; - - LLOG("alloc %p %u", hook_alloc_ptr, hook_alloc_sz); - - gMemProfileData.is_in_hook--; -} - -static void AllocMemHandlerHook(lua_State *L, lua_Debug *par) { - my_lua_Alloc_safe(); - lua_sethook(L, 0, 0, 0); -} - static void *my_lua_Alloc(void *ud, void *ptr, size_t osize, size_t nsize) { + ProfilerSession *s = (ProfilerSession *)ud; size_t realosize = (ptr) ? osize : 0; - if (gRunning == 0) { - lrealstopmemsafe(gL); - return gMemProfileData.oldAlloc(ud, ptr, osize, nsize); + if (!s->memRunning) { + void *ret = s->memData.oldAlloc(s->memData.oldAllocUd, ptr, osize, nsize); + lrealstopmemsafe(s->L); + return ret; } if (realosize < nsize) { - LLOG("my_lua_Alloc alloc %p %p %u %u", ud, ptr, osize, nsize); + LLOG("my_lua_Alloc alloc %p %p %u %u", ud, ptr, (unsigned)osize, (unsigned)nsize); - if (gMemProfileData.is_in_hook) { - return gMemProfileData.oldAlloc(ud, ptr, osize, nsize); + if (s->memData.is_in_hook) { + return s->memData.oldAlloc(s->memData.oldAllocUd, ptr, osize, nsize); } - // check stop if set sample count - if (gSampleCount != 0 && gSampleCount <= gMemProfileData.total) { + if (s->memSampleLimit != 0 && s->memSampleLimit <= s->memData.total) { LLOG("lrealstopmem..."); - lrealstopmem(gL); - return gMemProfileData.oldAlloc(ud, ptr, osize, nsize); + lrealstopmem(s->L); + return s->memData.oldAlloc(s->memData.oldAllocUd, ptr, osize, nsize); } - // is alloc size_t alloc_sz = nsize - realosize; - if (realosize > 0 && ptr) { - // remove old pointer - gMemProfileData.ptr2Callstack.erase(ptr); + s->memData.ptr2Callstack.erase(ptr); } - // 是否命中采样 - if (static_cast(alloc_sz) < gMemProfileData.nextSample) { - LLOG("not hit %ld %u", gMemProfileData.nextSample, alloc_sz); - gMemProfileData.nextSample -= static_cast(alloc_sz); - LLOG("nextSample %ld", gMemProfileData.nextSample); - return gMemProfileData.oldAlloc(ud, ptr, osize, nsize); + if (static_cast(alloc_sz) < s->memData.nextSample) { + s->memData.nextSample -= static_cast(alloc_sz); + return s->memData.oldAlloc(s->memData.oldAllocUd, ptr, osize, nsize); } - gMemProfileData.nextSample = gen_next_sample(); - LLOG("gen_next_sample nextSample %ld", gMemProfileData.nextSample); - - // start profile - gMemProfileData.total++; - - // 防止重入,get_cur_callstack是可能触发lua内存分配的。当再次重入,只是设置下hook,等原来hook退出后清空 - lua_sethook(gL, AllocMemHandlerHook, LUA_MASKCOUNT, 1); - - void *alloc_ptr = gMemProfileData.oldAlloc(ud, ptr, osize, nsize); - - // 记录参数 - gMemProfileData.hook_alloc_ptr = alloc_ptr; - gMemProfileData.hook_alloc_sz = alloc_sz; - - LLOG("prealloc %p %u", alloc_ptr, alloc_sz); + s->memData.nextSample = gen_next_sample(s); + s->memData.total++; + void *alloc_ptr = s->memData.oldAlloc(s->memData.oldAllocUd, ptr, osize, nsize); + s->memData.hook_alloc_ptr = alloc_ptr; + s->memData.hook_alloc_sz = alloc_sz; + request_hook(s, false, true); return alloc_ptr; } else if (realosize > nsize) { - LLOG("my_lua_Alloc free %p %p %u %u", ud, ptr, osize, nsize); - - // remove old pointer - auto it = gMemProfileData.ptr2Callstack.find(ptr); - if (it == gMemProfileData.ptr2Callstack.end()) { - return gMemProfileData.oldAlloc(ud, ptr, osize, nsize); + auto it = s->memData.ptr2Callstack.find(ptr); + if (it == s->memData.ptr2Callstack.end()) { + return s->memData.oldAlloc(s->memData.oldAllocUd, ptr, osize, nsize); } CallStack *cs = it->second; - gMemProfileData.ptr2Callstack.erase(it); - - auto it2 = gMemProfileData.callstack.find(cs); - if (it2 != gMemProfileData.callstack.end()) { - auto &mem_info = it2->second; - mem_info.frees++; + s->memData.ptr2Callstack.erase(it); - LLOG("free %p %u %u", ptr, realosize, nsize); + auto it2 = s->memData.callstack.find(cs); + if (it2 != s->memData.callstack.end()) { + it2->second.frees++; } - void *ret = gMemProfileData.oldAlloc(ud, ptr, osize, nsize); + void *ret = s->memData.oldAlloc(s->memData.oldAllocUd, ptr, osize, nsize); if (nsize > 0 && ret) { - // add new pointer - gMemProfileData.ptr2Callstack[ret] = cs; + s->memData.ptr2Callstack[ret] = cs; } - return ret; } else { - return gMemProfileData.oldAlloc(ud, ptr, osize, nsize); - } -} - -static std::string get_mem_filename(std::string path, const std::string &suffix) { - auto pos = path.find_last_of('.'); - if (pos == std::string::npos) { - return path; + return s->memData.oldAlloc(s->memData.oldAllocUd, ptr, osize, nsize); } - path.insert(pos, suffix); - return path; } static int lrealstartmemsafe(lua_State *L) { - if (gRunning) { - LERR("start again, failed"); + ProfilerSession *s = get_session(L, false); + if (!s) return -1; + if (s->memRunning) { + LERR("mem start again, failed"); return -1; } - gRunning = 1; - for (int i = 0; i < VALID_MIN_ID; i++) { - gString2Id[IGNORE_NAME[i]] = i; - gId2String[i] = IGNORE_NAME[i]; + if (s->string2Id.empty()) { + init_ignore_names(s); } - int fd = open(get_mem_filename(gFilename, "_ALLOC_SIZE").c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); + int fd = open(get_mem_filename(s->memFilename, "_ALLOC_SIZE").c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fd < 0) { - LERR("open file fail %s", gFilename.c_str()); + LERR("open file fail %s", s->memFilename.c_str()); return -1; } - gMemProfileData.alloc_size_fd = fd; + if (!write_pro_header(fd)) { close(fd); return -1; } + s->memData.alloc_size_fd = fd; - fd = open(get_mem_filename(gFilename, "_USAGE").c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); + fd = open(get_mem_filename(s->memFilename, "_USAGE").c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fd < 0) { - LERR("open file fail %s", gFilename.c_str()); + LERR("open file fail %s", s->memFilename.c_str()); + close(s->memData.alloc_size_fd); + s->memData.alloc_size_fd = -1; return -1; } - gMemProfileData.usage_fd = fd; + if (!write_pro_header(fd)) { + close(fd); + close(s->memData.alloc_size_fd); + s->memData.alloc_size_fd = -1; + return -1; + } + s->memData.usage_fd = fd; - gMemProfileData.total = 0; - for (auto iter = gMemProfileData.callstack.begin(); iter != gMemProfileData.callstack.end(); iter++) { + s->memData.total = 0; + for (auto iter = s->memData.callstack.begin(); iter != s->memData.callstack.end(); iter++) { delete iter->first; } - gMemProfileData.callstack.clear(); - gMemProfileData.ptr2Callstack.clear(); + s->memData.callstack.clear(); + s->memData.ptr2Callstack.clear(); - // replace the realloc - gMemProfileData.oldAlloc = lua_getallocf(L, NULL); - lua_setallocf(L, my_lua_Alloc, NULL); - gMemProfileData.rand = time(NULL); - // Step it forward 20 times for good measure + s->memData.oldAlloc = lua_getallocf(L, &s->memData.oldAllocUd); + lua_setallocf(L, my_lua_Alloc, s); + s->memData.rand = (uint64_t)time(NULL); for (int i = 0; i < 20; i++) { - gMemProfileData.rand = next_random(gMemProfileData.rand); + s->memData.rand = next_random(s->memData.rand); } - gMemProfileData.nextSample = gen_next_sample(); - gMemProfileData.is_in_hook = 0; - gMemProfileData.hook_alloc_ptr = 0; - gMemProfileData.hook_alloc_sz = 0; - + s->memData.nextSample = gen_next_sample(s); + s->memData.is_in_hook = 0; + s->memData.hook_alloc_ptr = 0; + s->memData.hook_alloc_sz = 0; + s->memRunning = true; return 0; } static void StartMemHandlerHook(lua_State *L, lua_Debug *par) { + (void)par; lrealstartmemsafe(L); - lua_sethook(L, 0, 0, 0); + ProfilerSession *s = get_session(L, false); + rearm_session_hook(s); } extern "C" int lrealstartmem(lua_State *L, int count, const char *file) { - if (gRunning) { - LERR("start again, failed"); + load_env_defaults(); + ProfilerSession *s = get_session(L, true); + if (s->memRunning) { + LERR("mem start again, failed"); + return -1; + } + if (!file) { + LERR("mem start missing file"); return -1; } - gL = L; - gSampleCount = count; - gFilename = file; - gMemProfileData.oldAlloc = NULL; - gMemProfileData.nextSample = 0; - - // lrealstartmem可能被注入调用,在StartMemHandlerHook里面执行具体的逻辑 - lua_sethook(gL, StartMemHandlerHook, LUA_MASKCOUNT, 1); - - LLOG("lstart %u %s", gSampleCount, file); + s->memProfileRate = gDefaultMemProfileRate; + s->memSampleLimit = count; + s->memFilename = file; + s->memData.oldAlloc = NULL; + s->memData.oldAllocUd = NULL; + s->memData.nextSample = 0; + lua_sethook(L, StartMemHandlerHook, LUA_MASKCOUNT, 1); + LLOG("lstartmem %u %s", s->memSampleLimit, file); return 0; } static int lstart_mem(lua_State *L) { - int count = (int) lua_tointeger(L, 1); + int count = (int)lua_tointeger(L, 1); const char *file = lua_tostring(L, 2); + if (lua_gettop(L) >= 3 && lua_isnumber(L, 3)) { + int rate = (int)lua_tointeger(L, 3); + if (rate > 0) gDefaultMemProfileRate = rate; + } int ret = lrealstartmem(L, count, file); lua_pushinteger(L, ret); return 1; } static int lstop_mem(lua_State *L) { - LLOG("lstop %s", gFilename.c_str()); + ProfilerSession *s = get_session(L, false); + LLOG("lstop_mem %s", s ? s->memFilename.c_str() : ""); int ret = lrealstopmem(L); lrealstopmemsafe(L); lua_pushinteger(L, ret); return 1; } -//////////////////////////////////mem profiler end////////////////////////////////////////// +static int lset_config(lua_State *L) { + load_env_defaults(); + if (lua_istable(L, 1)) { + lua_getfield(L, 1, "cpu_interval_ms"); + if (lua_isnumber(L, -1)) { + int v = (int)lua_tointeger(L, -1); + if (v > 0) gDefaultCpuIntervalMs = v; + } + lua_pop(L, 1); -extern "C" int luaopen_libplua(lua_State *L) { - luaL_checkversion(L); - luaL_Reg l[] = { - // for cpu - {"start", lstart}, - {"stop", lstop}, + lua_getfield(L, 1, "cpu_hook_count"); + if (lua_isnumber(L, -1)) { + int v = (int)lua_tointeger(L, -1); + if (v > 0) gDefaultCpuHookCount = v; + } + lua_pop(L, 1); - // for memory - {"start_mem", lstart_mem}, - {"stop_mem", lstop_mem}, + lua_getfield(L, 1, "mem_rate"); + if (lua_isnumber(L, -1)) { + int v = (int)lua_tointeger(L, -1); + if (v > 0) gDefaultMemProfileRate = v; + } + lua_pop(L, 1); + + lua_getfield(L, 1, "cpu_backend"); + if (lua_isstring(L, -1)) { + const char *b = lua_tostring(L, -1); + if (b && (strcmp(b, "count") == 0 || strcmp(b, "maskcount") == 0)) { + gForceCountBackend = 1; + } else if (b && strcmp(b, "sigprof") == 0) { +#if defined(PLUA_HAS_POSIX_PROF) + gForceCountBackend = 0; +#endif + } + } + lua_pop(L, 1); + } + lua_pushinteger(L, 0); + return 1; +} + +static int lget_config(lua_State *L) { + load_env_defaults(); + lua_newtable(L); + lua_pushinteger(L, gDefaultCpuIntervalMs); + lua_setfield(L, -2, "cpu_interval_ms"); + lua_pushinteger(L, gDefaultCpuHookCount); + lua_setfield(L, -2, "cpu_hook_count"); + lua_pushinteger(L, gDefaultMemProfileRate); + lua_setfield(L, -2, "mem_rate"); +#if defined(PLUA_HAS_POSIX_PROF) + lua_pushstring(L, gForceCountBackend ? "count" : "sigprof"); +#else + lua_pushstring(L, "count"); +#endif + lua_setfield(L, -2, "cpu_backend"); + return 1; +} +static void register_plua_api(lua_State *L) { + luaL_Reg l[] = { + {"start", lstart}, + {"stop", lstop}, + {"start_mem", lstart_mem}, + {"stop_mem", lstop_mem}, + {"set_config", lset_config}, + {"get_config", lget_config}, {NULL, NULL}, }; luaL_newlib(L, l); +} + +extern "C" int luaopen_libplua(lua_State *L) { + luaL_checkversion(L); + register_plua_api(L); return 1; } + +// Alias so `require "plua"` also works when the shared library is named accordingly. +extern "C" int luaopen_plua(lua_State *L) { + return luaopen_libplua(L); +} From a27d07f9f023f11d5ba753b58bb7d00565d6bf40 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Tue, 3 Nov 2026 20:29:35 +0800 Subject: [PATCH 2/6] feat(tools): parse versioned .pro and drop deprecated ioutil Accept PLUA magic/version header while remaining compatible with legacy profiles; use os.ReadFile/os.WriteFile. --- tools/plua.go | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/tools/plua.go b/tools/plua.go index 9af8d0b..8fcd3ea 100644 --- a/tools/plua.go +++ b/tools/plua.go @@ -4,7 +4,6 @@ import ( "encoding/binary" "flag" "fmt" - "io/ioutil" "math" "os" "strings" @@ -13,6 +12,7 @@ import ( const ( MAX_FUNC_NAME_SIZE = 127 MAX_STACK_SIZE = 64 + proMagic = 0x41554C50 // 'PLUA' little-endian ) type CallStack struct { @@ -22,6 +22,7 @@ type CallStack struct { } type FileData struct { + version uint32 str2id map[string]int id2str map[int]string callstack []CallStack @@ -59,8 +60,7 @@ func main() { } func parse(filename string) (*FileData, bool) { - - data, err := ioutil.ReadFile(filename) + data, err := os.ReadFile(filename) if err != nil { fmt.Printf("ReadFile fail %v\n", err) return nil, false @@ -71,6 +71,20 @@ func parse(filename string) (*FileData, bool) { return nil, false } + headerSize := 0 + version := uint32(0) + if len(data) >= 8 { + magic := binary.LittleEndian.Uint32(data[0:4]) + if magic == proMagic { + version = binary.LittleEndian.Uint32(data[4:8]) + if version < 1 { + fmt.Printf("unsupported .pro version %v\n", version) + return nil, false + } + headerSize = 8 + } + } + namemaplen := int(binary.LittleEndian.Uint32(data[len(data)-4 : len(data)])) if namemaplen < 0 { fmt.Printf("name map len fail %v\n", namemaplen) @@ -84,7 +98,7 @@ func parse(filename string) (*FileData, bool) { end := len(data) - 4 for i := 0; i < len(data) && namenum < namemaplen; i++ { start := end - 4 - if start < 0 || end < 0 { + if start < headerSize || end < headerSize { fmt.Printf("get id fail %v %v\n", start, end) return nil, false } @@ -92,7 +106,7 @@ func parse(filename string) (*FileData, bool) { end -= 4 start = end - 4 - if start < 0 || end < 0 { + if start < headerSize || end < headerSize { fmt.Printf("get name len fail %v %v\n", start, end) return nil, false } @@ -105,6 +119,10 @@ func parse(filename string) (*FileData, bool) { } start = end - namelen + if start < headerSize { + fmt.Printf("name out of range\n") + return nil, false + } str := string(data[start:end]) end -= namelen @@ -115,16 +133,24 @@ func parse(filename string) (*FileData, bool) { } callstack := make([]CallStack, 0) - for end > 0 { + for end > headerSize { stacks := make([]int, MAX_STACK_SIZE) for i := 0; i < MAX_STACK_SIZE; i++ { start := end - 4 + if start < headerSize { + fmt.Printf("stack underflow\n") + return nil, false + } stack := int(binary.LittleEndian.Uint32(data[start:end])) end -= 4 stacks[MAX_STACK_SIZE-i-1] = stack } start := end - 4 + if start < headerSize { + fmt.Printf("deps underflow\n") + return nil, false + } deps := int(binary.LittleEndian.Uint32(data[start:end])) end -= 4 @@ -134,6 +160,10 @@ func parse(filename string) (*FileData, bool) { } start = end - 4 + if start < headerSize { + fmt.Printf("count underflow\n") + return nil, false + } count := int(binary.LittleEndian.Uint32(data[start:end])) end -= 4 @@ -152,6 +182,7 @@ func parse(filename string) (*FileData, bool) { } filedata := &FileData{ + version: version, str2id: str2id, id2str: id2str, callstack: callstack, @@ -161,7 +192,6 @@ func parse(filename string) (*FileData, bool) { } func showpprof(filedata *FileData, filename string) { - var output []byte output = append(output, []byte("--- symbol\n")...) @@ -239,7 +269,7 @@ func showcollapsed(filedata *FileData, filename string) { total += cs.count } - err := ioutil.WriteFile(filename, []byte(b.String()), 0644) + err := os.WriteFile(filename, []byte(b.String()), 0644) if err != nil { fmt.Printf("write flame collapsed file fail: %v\n", err) os.Exit(1) From a7df5075076086cdd259e479d8252b447104b338 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Tue, 3 Nov 2026 20:29:35 +0800 Subject: [PATCH 3/6] test: multi-state coverage and cross-platform cpath suffixes - C++ harness: two-state CPU/mem, cross-mode, stop-one-leaves-other, independent CPU+mem on one state - Lua unit tests: config API, PLUA magic, .dll/.dylib cpath candidates --- test/CMakeLists.txt | 3 + test/test_cpu.lua | 2 +- test/test_mem.lua | 2 +- test/test_unit.lua | 51 +++++++++++++- test/unit_test.cpp | 168 +++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 214 insertions(+), 12 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a979787..df71732 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -8,3 +8,6 @@ if(LUA_EXECUTABLE) add_test(NAME LuaUnitTest COMMAND ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_unit.lua WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) endif() + +# Multi-state tests need the C++ harness (two lua_State* in one process) +add_test(NAME CppMultiStateTest COMMAND unit_test --multistate) diff --git a/test/test_cpu.lua b/test/test_cpu.lua index ba558a6..0b3e4f5 100644 --- a/test/test_cpu.lua +++ b/test/test_cpu.lua @@ -1,4 +1,4 @@ -package.cpath = "../bin/?.so;" .. package.cpath +package.cpath = "../bin/?.so;../bin/?.dylib;../bin/?.dll;" .. package.cpath function test() diff --git a/test/test_mem.lua b/test/test_mem.lua index e93104f..6a83c8b 100644 --- a/test/test_mem.lua +++ b/test/test_mem.lua @@ -1,4 +1,4 @@ -package.cpath = "../bin/?.so;" .. package.cpath +package.cpath = "../bin/?.so;../bin/?.dylib;../bin/?.dll;" .. package.cpath gt = {} diff --git a/test/test_unit.lua b/test/test_unit.lua index 248fc09..ed3be45 100644 --- a/test/test_unit.lua +++ b/test/test_unit.lua @@ -1,9 +1,17 @@ -- Unit tests for pLua local cpath_candidates = { "../bin/?.so", + "../bin/?.dylib", + "../bin/?.dll", "../../bin/?.so", + "../../bin/?.dylib", + "../../bin/?.dll", "bin/?.so", - "./?.so" + "bin/?.dylib", + "bin/?.dll", + "./?.so", + "./?.dylib", + "./?.dll", } for _, p in ipairs(cpath_candidates) do package.cpath = p .. ";" .. package.cpath @@ -21,6 +29,14 @@ local function remove_file(filepath) os.remove(filepath) end +local function read_magic(filepath) + local f = io.open(filepath, "rb") + if not f then return nil end + local magic = f:read(4) + f:close() + return magic +end + print("========================================") print("Running pLua Unit Tests") print("========================================") @@ -34,8 +50,19 @@ assert(type(plua.start) == "function", "Missing function plua.start") assert(type(plua.stop) == "function", "Missing function plua.stop") assert(type(plua.start_mem) == "function", "Missing function plua.start_mem") assert(type(plua.stop_mem) == "function", "Missing function plua.stop_mem") +assert(type(plua.set_config) == "function", "Missing function plua.set_config") +assert(type(plua.get_config) == "function", "Missing function plua.get_config") print(" -> Passed: libplua module and API functions loaded successfully") +-- 1b. Config API +print("[Test 1b] Config API...") +local cfg = plua.get_config() +assert(type(cfg) == "table", "get_config should return a table") +assert(type(cfg.cpu_interval_ms) == "number", "cpu_interval_ms missing") +assert(type(cfg.mem_rate) == "number", "mem_rate missing") +assert(plua.set_config({ cpu_interval_ms = cfg.cpu_interval_ms, mem_rate = cfg.mem_rate }) == 0) +print(" -> Passed: config get/set OK (backend=" .. tostring(cfg.cpu_backend) .. ")") + -- 2. Test CPU Profiling print("[Test 2] CPU Profiling...") local cpu_out = "unit_cpu_test.pro" @@ -66,6 +93,7 @@ ret = plua.stop() assert(ret == 0, "plua.stop returned error: " .. tostring(ret)) assert(file_exists_and_not_empty(cpu_out), "CPU profile file was not created or is empty: " .. cpu_out) +assert(read_magic(cpu_out) == "PLUA", "CPU profile missing PLUA magic header") print(" -> Passed: CPU profile generated successfully (" .. cpu_out .. ")") -- 3. Test Memory Profiling @@ -90,9 +118,10 @@ assert(stop_ret == 0, "plua.stop_mem returned error: " .. tostring(stop_ret)) assert(file_exists_and_not_empty(mem_alloc), "Memory ALLOC_SIZE profile not created: " .. mem_alloc) assert(file_exists_and_not_empty(mem_usage), "Memory USAGE profile not created: " .. mem_usage) +assert(read_magic(mem_alloc) == "PLUA", "mem profile missing PLUA magic") print(" -> Passed: Memory profiles generated successfully (" .. mem_alloc .. ", " .. mem_usage .. ")") --- 4. Test Guard Against Concurrent Profiling +-- 4. Test Guard Against Concurrent Profiling (same state, same mode) print("[Test 4] Concurrent Profiling Guards...") local guard_out = "unit_guard_test.pro" remove_file(guard_out) @@ -107,6 +136,23 @@ plua.stop() remove_file(guard_out) print(" -> Passed: Re-entrancy guard correctly rejected concurrent start") +-- 5. Independent CPU + mem on same state +print("[Test 5] Independent CPU + Memory...") +remove_file(cpu_out) +remove_file(mem_alloc) +remove_file(mem_usage) +assert(plua.start(0, cpu_out) == 0) +assert(plua.start_mem(0, mem_out) == 0) +for i = 1, 20 do cpu_work() end +holder = {} +for i = 1, 20000 do holder[i] = { i = i } end +collectgarbage("collect") +assert(plua.stop() == 0) +assert(plua.stop_mem() == 0) +assert(file_exists_and_not_empty(cpu_out)) +assert(file_exists_and_not_empty(mem_alloc)) +print(" -> Passed: CPU and mem can run together on one state") + -- Cleanup remove_file(cpu_out) remove_file(mem_alloc) @@ -116,3 +162,4 @@ remove_file("plua.log") print("========================================") print("ALL UNIT TESTS PASSED SUCCESSFULLY!") print("========================================") +print("Note: multi-lua_State coverage lives in the C++ unit_test (--multistate).") diff --git a/test/unit_test.cpp b/test/unit_test.cpp index e62815c..5616be3 100644 --- a/test/unit_test.cpp +++ b/test/unit_test.cpp @@ -2,7 +2,14 @@ #include #include #include +#include +#include + +#if !defined(_WIN32) #include +#else +#include +#endif extern "C" { #include "lua.h" @@ -25,15 +32,17 @@ static bool file_exists_and_non_empty(const std::string &path) { return f.tellg() > 0; } -int main() { +static void register_plua(lua_State *L) { + luaL_requiref(L, "libplua", luaopen_libplua, 1); + lua_pop(L, 1); +} + +static int run_basic_tests() { std::cout << "[C++ Test] Initializing Lua state..." << std::endl; lua_State *L = luaL_newstate(); ASSERT_TRUE(L != nullptr); luaL_openlibs(L); - - // Register plua module directly into package.loaded - luaL_requiref(L, "libplua", luaopen_libplua, 1); - lua_pop(L, 1); + register_plua(L); std::cout << "[C++ Test] Testing CPU profiling..." << std::endl; const char *cpu_file = "cpp_test_cpu.pro"; @@ -46,8 +55,10 @@ int main() { "for i = 1, 50 do fib(22) end\n" "p.stop()\n" ); + if (r != 0) { + std::cerr << lua_tostring(L, -1) << std::endl; + } ASSERT_TRUE(r == 0); - ASSERT_TRUE(file_exists_and_non_empty(cpu_file)); std::remove(cpu_file); std::cout << " -> CPU profiling OK" << std::endl; @@ -66,8 +77,10 @@ int main() { "collectgarbage('collect')\n" "p.stop_mem()\n" ); + if (r != 0) { + std::cerr << lua_tostring(L, -1) << std::endl; + } ASSERT_TRUE(r == 0); - ASSERT_TRUE(file_exists_and_non_empty(mem_alloc)); ASSERT_TRUE(file_exists_and_non_empty(mem_usage)); std::remove(mem_alloc); @@ -75,7 +88,146 @@ int main() { std::remove("plua.log"); std::cout << " -> Memory profiling OK" << std::endl; + std::cout << "[C++ Test] Testing independent CPU + mem on same state..." << std::endl; + std::remove(cpu_file); + std::remove(mem_alloc); + std::remove(mem_usage); + r = luaL_dostring(L, + "local function fib(n) if n <= 1 then return n end return fib(n-1) + fib(n-2) end\n" + "local p = require('libplua')\n" + "p.start(0, 'cpp_test_cpu.pro')\n" + "p.start_mem(0, 'cpp_test_mem.pro')\n" + "local t = {}\n" + "for i = 1, 20000 do t[i] = { id = i } end\n" + "for i = 1, 20 do fib(20) end\n" + "collectgarbage('collect')\n" + "p.stop()\n" + "p.stop_mem()\n" + ); + if (r != 0) { + std::cerr << lua_tostring(L, -1) << std::endl; + } + ASSERT_TRUE(r == 0); + ASSERT_TRUE(file_exists_and_non_empty(cpu_file)); + ASSERT_TRUE(file_exists_and_non_empty(mem_alloc)); + std::remove(cpu_file); + std::remove(mem_alloc); + std::remove(mem_usage); + std::cout << " -> Independent CPU+mem OK" << std::endl; + lua_close(L); - std::cout << "[C++ Test] All C++ unit tests passed!" << std::endl; + std::cout << "[C++ Test] All basic C++ unit tests passed!" << std::endl; return 0; } + +static void busy_work(lua_State *L) { + int r = luaL_dostring(L, + "local function fib(n) if n <= 1 then return n end return fib(n-1) + fib(n-2) end\n" + "for i = 1, 30 do fib(20) end\n" + ); + ASSERT_TRUE(r == 0); +} + +static void mem_work(lua_State *L) { + int r = luaL_dostring(L, + "local t = {}\n" + "for i = 1, 30000 do t[i] = { id = i, s = tostring(i) } end\n" + "collectgarbage('collect')\n" + ); + ASSERT_TRUE(r == 0); +} + +static int run_multistate_tests() { + std::cout << "[C++ MultiState] two states CPU..." << std::endl; + lua_State *L1 = luaL_newstate(); + lua_State *L2 = luaL_newstate(); + ASSERT_TRUE(L1 && L2); + luaL_openlibs(L1); + luaL_openlibs(L2); + register_plua(L1); + register_plua(L2); + + const char *cpu1 = "ms_cpu1.pro"; + const char *cpu2 = "ms_cpu2.pro"; + std::remove(cpu1); + std::remove(cpu2); + + ASSERT_TRUE(luaL_dostring(L1, "require('libplua').start(0, 'ms_cpu1.pro')") == 0); + ASSERT_TRUE(luaL_dostring(L2, "require('libplua').start(0, 'ms_cpu2.pro')") == 0); + busy_work(L1); + busy_work(L2); + ASSERT_TRUE(luaL_dostring(L1, "require('libplua').stop()") == 0); + ASSERT_TRUE(luaL_dostring(L2, "require('libplua').stop()") == 0); + ASSERT_TRUE(file_exists_and_non_empty(cpu1)); + ASSERT_TRUE(file_exists_and_non_empty(cpu2)); + std::remove(cpu1); + std::remove(cpu2); + std::cout << " -> two states CPU OK" << std::endl; + + std::cout << "[C++ MultiState] two states mem..." << std::endl; + const char *m1a = "ms_mem1_ALLOC_SIZE.pro"; + const char *m1u = "ms_mem1_USAGE.pro"; + const char *m2a = "ms_mem2_ALLOC_SIZE.pro"; + const char *m2u = "ms_mem2_USAGE.pro"; + std::remove(m1a); std::remove(m1u); std::remove(m2a); std::remove(m2u); + ASSERT_TRUE(luaL_dostring(L1, "require('libplua').start_mem(0, 'ms_mem1.pro')") == 0); + ASSERT_TRUE(luaL_dostring(L2, "require('libplua').start_mem(0, 'ms_mem2.pro')") == 0); + mem_work(L1); + mem_work(L2); + ASSERT_TRUE(luaL_dostring(L1, "require('libplua').stop_mem()") == 0); + ASSERT_TRUE(luaL_dostring(L2, "require('libplua').stop_mem()") == 0); + ASSERT_TRUE(file_exists_and_non_empty(m1a)); + ASSERT_TRUE(file_exists_and_non_empty(m2a)); + std::remove(m1a); std::remove(m1u); std::remove(m2a); std::remove(m2u); + std::cout << " -> two states mem OK" << std::endl; + + std::cout << "[C++ MultiState] one CPU + one mem across states..." << std::endl; + std::remove(cpu1); + std::remove(m2a); std::remove(m2u); + ASSERT_TRUE(luaL_dostring(L1, "require('libplua').start(0, 'ms_cpu1.pro')") == 0); + ASSERT_TRUE(luaL_dostring(L2, "require('libplua').start_mem(0, 'ms_mem2.pro')") == 0); + busy_work(L1); + mem_work(L2); + ASSERT_TRUE(luaL_dostring(L1, "require('libplua').stop()") == 0); + ASSERT_TRUE(luaL_dostring(L2, "require('libplua').stop_mem()") == 0); + ASSERT_TRUE(file_exists_and_non_empty(cpu1)); + ASSERT_TRUE(file_exists_and_non_empty(m2a)); + std::remove(cpu1); std::remove(m2a); std::remove(m2u); + std::cout << " -> cross-mode OK" << std::endl; + + std::cout << "[C++ MultiState] stop one leaves other..." << std::endl; + std::remove(cpu1); + std::remove(cpu2); + ASSERT_TRUE(luaL_dostring(L1, "require('libplua').start(0, 'ms_cpu1.pro')") == 0); + ASSERT_TRUE(luaL_dostring(L2, "require('libplua').start(0, 'ms_cpu2.pro')") == 0); + busy_work(L1); + busy_work(L2); + ASSERT_TRUE(luaL_dostring(L1, "require('libplua').stop()") == 0); + ASSERT_TRUE(file_exists_and_non_empty(cpu1)); + // L2 still running — more work then stop + busy_work(L2); + ASSERT_TRUE(luaL_dostring(L2, "require('libplua').stop()") == 0); + ASSERT_TRUE(file_exists_and_non_empty(cpu2)); + std::remove(cpu1); std::remove(cpu2); + std::cout << " -> stop-one-leaves-other OK" << std::endl; + + lua_close(L1); + lua_close(L2); + std::remove("plua.log"); + std::cout << "[C++ MultiState] All multi-state tests passed!" << std::endl; + return 0; +} + +int main(int argc, char **argv) { + bool multistate = false; + for (int i = 1; i < argc; i++) { + if (std::strcmp(argv[i], "--multistate") == 0) multistate = true; + } + if (multistate) { + return run_multistate_tests(); + } + // Default: run both suites so a plain `unit_test` invocation is thorough + int rc = run_basic_tests(); + if (rc != 0) return rc; + return run_multistate_tests(); +} From b675dc351ab5a6b8e89f98a49847cc88b2b8c083 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Tue, 3 Nov 2026 20:29:35 +0800 Subject: [PATCH 4/6] ci: add macOS (build+test) and Windows (build) to Actions matrix Keep existing Ubuntu job with examples/visualization; macOS runs ctest; Windows builds libplua.dll (tests optional/deferred when Lua toolchain is incomplete). --- .github/workflows/ccpp.yml | 89 ++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml index 7071812..ff923f2 100644 --- a/.github/workflows/ccpp.yml +++ b/.github/workflows/ccpp.yml @@ -7,9 +7,8 @@ on: branches: [ "**" ] jobs: - build-and-test: + ubuntu: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -22,16 +21,13 @@ jobs: echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - name: Configure CMake - run: | - cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON - name: Build - run: | - cmake --build build --config Release + run: cmake --build build --config Release - name: Run Unit Tests - run: | - ctest --test-dir build --output-on-failure + run: ctest --test-dir build --output-on-failure - name: Run Example & Test Visualization Tools run: | @@ -40,3 +36,80 @@ jobs: lua test_mem.lua || lua5.3 test_mem.lua cd ../tools ./show.sh ../test + + macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Dependencies + run: | + brew update + brew install lua go || true + # Ensure lua headers/libs are discoverable + brew list lua + + - name: Configure CMake + run: | + LUA_PREFIX="$(brew --prefix lua)" + cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON \ + -DLUA_INCLUDE_DIR="${LUA_PREFIX}/include/lua" \ + -DLUA_LIBRARIES="${LUA_PREFIX}/lib/liblua.dylib" \ + || cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + + - name: Build + run: cmake --build build --config Release + + - name: Run Unit Tests + run: ctest --test-dir build --output-on-failure + + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Install Lua (via chocolatey / vcpkg fallback) + shell: pwsh + run: | + choco install lua -y --no-progress + # Headers: download Lua source for includes if needed + if (-not (Test-Path "C:\Lua\include\lua.h")) { + Invoke-WebRequest -Uri "https://www.lua.org/ftp/lua-5.4.7.tar.gz" -OutFile "$env:RUNNER_TEMP\lua.tgz" + tar -xzf "$env:RUNNER_TEMP\lua.tgz" -C "$env:RUNNER_TEMP" + New-Item -ItemType Directory -Force -Path "C:\Lua\include" | Out-Null + Copy-Item "$env:RUNNER_TEMP\lua-5.4.7\src\*.h" "C:\Lua\include\" + } + + - name: Configure CMake + shell: pwsh + run: | + $inc = "C:\ProgramData\chocolatey\lib\lua\tools\include" + if (-not (Test-Path $inc)) { $inc = "C:\Lua\include" } + $lib = "C:\ProgramData\chocolatey\lib\lua\tools" + cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF ` + "-DLUA_INCLUDE_DIR=$inc" ` + "-DLUA_LIBRARIES=$lib\lua54.lib" ` + 2>$null + if ($LASTEXITCODE -ne 0) { + # Fallback: try FindLua defaults after choco + cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF + } + + - name: Build + shell: pwsh + run: | + cmake --build build --config Release + if ($LASTEXITCODE -ne 0) { + Write-Host "Windows build failed — CI records the failure; see README for MSVC+Lua setup notes." + exit 1 + } + + - name: Check DLL artifact + shell: pwsh + run: | + Get-ChildItem -Recurse bin, build -Filter "*.dll" -ErrorAction SilentlyContinue | Format-Table FullName, Length From a88f13327a9a4d105be610d8427308931d8a0f02 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Tue, 3 Nov 2026 20:29:35 +0800 Subject: [PATCH 5/6] docs: multi-state, platforms, rates, embedding vs hookso, Lua versions Document that multi-state is not multi-OS-thread safe; hookso is Linux only; Lua 5.1/JIT are unsupported; sample-rate API and env vars. --- .gitignore | 3 + README.md | 159 ++++++++++++++++++++++++++------------------- README_CN.md | 177 +++++++-------------------------------------------- 3 files changed, 122 insertions(+), 217 deletions(-) diff --git a/.gitignore b/.gitignore index e1f67dd..b2b691f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ plua.log tools/flamegraph.pl tools/pprof tools/plua + +build2 +build_asan diff --git a/README.md b/README.md index 67341de..89d6743 100755 --- a/README.md +++ b/README.md @@ -12,16 +12,31 @@ Lua Performance Profiler & Memory Profiler. Similar to `gperftools`, **pLua** profiles CPU hotspots and memory allocations for Lua programs. ## Features -- **Easy to Use**: Only a few lines of code to start profiling, or inject into running processes via [hookso](https://github.com/esrrhs/hookso) without modifying code. -- **Accurate**: Uses periodic timer sampling (`ITIMER_PROF`) rather than Lua line hooks to capture execution hotspots accurately and exclude sleep/idle time. +- **Easy to Use**: Only a few lines of code to start profiling. On Linux you can also inject into running processes via [hookso](https://github.com/esrrhs/hookso) without modifying code. +- **Accurate**: On POSIX, uses periodic timer sampling (`ITIMER_PROF` / `SIGPROF`) rather than only Lua line hooks, capturing execution hotspots and excluding sleep/idle time. On Windows (and optionally elsewhere) falls back to `LUA_MASKCOUNT`-based sampling. +- **Multi-state**: Each `lua_State` has its own profiler session. You can profile several states in one process. **Multi-state is not multi-OS-thread safe** — do not call pLua from multiple OS threads concurrently. +- **Independent CPU / Memory**: CPU and memory profiling use separate running flags; they can run at the same time on one state. - **Lightweight**: Sampling approach minimizes runtime overhead on the host process. - **Intuitive**: Outputs call graphs, compatible with `pprof` (`gperftools`), and generates [FlameGraph](https://github.com/brendangregg/FlameGraph) SVGs. --- -## Dependencies & Installation +## Platforms + +| Platform | Library artifact | CPU backend | hookso injection | Notes | +|----------|------------------|-------------|------------------|-------| +| Linux | `bin/libplua.so` | `SIGPROF` (default) or `LUA_MASKCOUNT` | Supported | Primary platform | +| macOS | `bin/libplua.so` (also loadable as `.dylib` via cpath) | `SIGPROF` (default) or count | **Not supported** | Embedding OK | +| Windows | `bin/libplua.dll` | `LUA_MASKCOUNT` | **Not supported** | Embedding OK | + +### Lua version support +pLua targets **Lua 5.2 / 5.3 / 5.4** (uses `luaL_newlib` / `luaL_checkversion` / `luaL_requiref`). -pLua requires Lua development headers and libraries (Lua 5.1/5.2/5.3/5.4 or LuaJIT). Please install the Lua dependency package using your system package manager: +**Lua 5.1 and LuaJIT are not supported** by the current module loader API. Older README claims of 5.1/JIT support were inaccurate. + +--- + +## Dependencies & Installation ### Ubuntu / Debian ```shell @@ -31,7 +46,6 @@ sudo apt-get install -y lua5.3 liblua5.3-dev ### CentOS / RHEL / Fedora ```shell -# CentOS / RHEL (EPEL) sudo yum install -y lua-devel ``` @@ -40,125 +54,153 @@ sudo yum install -y lua-devel brew install lua ``` +### Windows +Install Lua 5.4 development headers/libs (vcpkg, Chocolatey, or a manual build), then point CMake at them via `LUA_INCLUDE_DIR` / `LUA_LIBRARIES` if needed. + --- ## Build -Compile using CMake: - ```shell mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . ``` -Compiled library will be placed in `bin/libplua.so`. If Go is available, the `plua` profile converter tool will also be built automatically into `bin/plua`. +Artifacts land in `bin/`: +- Linux/macOS: `libplua.so` +- Windows: `libplua.dll` +- If Go is available: `plua` profile converter --- ## Unit Testing -Run unit tests via CTest: - ```shell cd build ctest --output-on-failure ``` -Or run the Lua unit test directly: +Or run the Lua unit test directly (after build): ```shell cd test lua test_unit.lua ``` +Multi-`lua_State` coverage is in the C++ harness (`unit_test` / `unit_test --multistate`). + --- ## Usage +### Module name +```lua +local p = require "libplua" -- preferred (matches libplua.so/.dll) +-- If you ship a copy/symlink named plua.so, require "plua" also works (luaopen_plua). +``` + +Add the library directory to `package.cpath` as needed: +```lua +package.cpath = "./bin/?.so;./bin/?.dylib;./bin/?.dll;" .. package.cpath +``` + ### 1. Collect CPU Profile Data -#### Option A: Embed in Lua Code +#### Embed in Lua Code ```lua --- Load libplua.so local p = require "libplua" --- Start CPU profiling --- Arg 1: sampling duration in seconds (0 = profile until stopped) +-- Arg 1: sampling duration in seconds (0 = until stopped) -- Arg 2: output profile file path +-- Arg 3 (optional): CPU sample interval in milliseconds (default 10, or PLUA_CPU_INTERVAL_MS) p.start(0, "call.pro") do_some_thing() --- Stop profiling and write out profile file p.stop() ``` -#### Option B: Inject via [hookso](https://github.com/esrrhs/hookso) +#### Inject via [hookso](https://github.com/esrrhs/hookso) (Linux only) ```shell -# a) Retrieve lua_State pointer from the target process (e.g. from lua_settop argument) -./hookso arg $PID xxx.so lua_settop 1 -123456 - -# b) Load libplua.so into the target process +./hookso arg $PID xxx.so lua_settop 1 ./hookso dlopen $PID ./libplua.so - -# c) Call lrealstart to manually start profiling: lrealstart(L, 0, "./call.pro") -./hookso call $PID libplua.so lrealstart i=123456 i=0 s="./call.pro" - -# d) Call lrealstop to stop profiling -./hookso call $PID libplua.so lrealstop i=123456 +./hookso call $PID libplua.so lrealstart i= i=0 s="./call.pro" +./hookso call $PID libplua.so lrealstop i= ``` +hookso injection is **not supported on macOS or Windows**. --- ### 2. Collect Memory Profile Data -#### Option A: Embed in Lua Code ```lua --- Load libplua.so local p = require "libplua" --- Start memory profiling --- Arg 1: sampling duration in seconds (0 = profile until stopped) --- Arg 2: prefix for output files (will generate _ALLOC_SIZE.pro and _USAGE.pro) +-- Arg 1: max samples (0 = until stopped) +-- Arg 2: output prefix -> _ALLOC_SIZE.pro and _USAGE.pro +-- Arg 3 (optional): sampling rate in bytes (default 524288 / 512KiB, or PLUA_MEM_RATE) p.start_mem(0, "mem.pro") do_some_thing() +collectgarbage("collect") -- optional, for retained-memory view +p.stop_mem() +``` --- Optional: trigger GC before stopping if analyzing retained memory -collectgarbage("collect") +--- --- Stop profiling and output result files -p.stop_mem() +### 3. Multi lua_State + +Each state keeps an independent session (CPU and/or mem). Stopping one state does not stop others. + +```lua +-- In host C/C++: create L1, L2, require libplua in each, call start/stop per state. ``` -#### Option B: Inject via [hookso](https://github.com/esrrhs/hookso) -```shell -# a) Retrieve lua_State pointer -./hookso arg $PID xxx.so lua_settop 1 -123456 +**Thread safety:** pLua is safe for multiple `lua_State` values used from **one OS thread** (or with external synchronization). It is **not** safe to drive two states from two OS threads without external locking. -# b) Load libplua.so -./hookso dlopen $PID ./libplua.so +--- -# c) Call lrealstartmem: lrealstartmem(L, 0, "./mem.pro") -./hookso call $PID libplua.so lrealstartmem i=123456 i=0 s="./mem.pro" +### 4. Configurable sample rates -# d) Call lrealstopmem to stop -./hookso call $PID libplua.so lrealstopmem i=123456 +Lua API: +```lua +p.set_config({ + cpu_interval_ms = 10, -- SIGPROF interval (POSIX) + cpu_hook_count = 1000, -- LUA_MASKCOUNT step (Windows / fallback) + mem_rate = 524288, -- bytes between mem samples + cpu_backend = "sigprof" -- or "count" / "maskcount" +}) +print(p.get_config().cpu_backend) ``` +Environment variables (read once at first use): +| Variable | Meaning | Default | +|----------|---------|---------| +| `PLUA_CPU_INTERVAL_MS` | POSIX timer interval (ms) | `10` | +| `PLUA_CPU_HOOK_COUNT` | `LUA_MASKCOUNT` instructions per sample | `1000` | +| `PLUA_MEM_RATE` | Memory sampling period (bytes) | `524288` | +| `PLUA_CPU_BACKEND` | `sigprof` or `count` | `sigprof` on POSIX, `count` on Windows | + --- -## Visualization & Tools +## .pro file format + +Versioned profiles start with: +- magic `PLUA` (4 bytes, little-endian `0x41554C50`) +- version `uint32` LE (currently `1`) +- then the legacy payload (call stacks + trailing string table) + +`tools/plua` accepts both v1 (with header) and legacy files (no header, treated as version 0). + +--- -The `tools/` directory provides utilities to convert `.pro` profile data into FlameGraph SVGs and call graph images. +## Visualization & Tools ### Prerequisites (Optional) -- **FlameGraph**: `show.sh` will automatically fetch `flamegraph.pl` from GitHub if not found locally or in `PATH`. On CentOS/RHEL, you may need `yum install perl-open`. -- **Graphviz** (for call graph PNG): `sudo apt install graphviz` / `sudo yum install graphviz` -- **pprof** (for call graph DOT/PNG): `go install github.com/google/pprof@latest` (Ubuntu ≤24.04: `sudo apt install google-perftools`; RHEL: `sudo yum install gperftools`) +- **FlameGraph**: `show.sh` fetches `flamegraph.pl` if needed +- **Graphviz**: for call graph PNG +- **pprof**: `go install github.com/google/pprof@latest` ### Generate Visualizations ```shell @@ -166,25 +208,14 @@ cd tools ./show.sh ../test ``` -This generates: -- `.fl`: Folded stack traces -- `.svg`: Interactive SVG flame graph -- `.prof`: Symbolized pprof profile -- `.dot`: Graphviz call graph -- `.png`: Rendered PNG call graph (if graphviz and pprof are installed) - --- ## Examples ### CPU FlameGraph & Call Graph -Generated from `test/test_cpu.lua`: - ![call](test/call.png) ### Memory Allocation Graph -Generated from `test/test_mem.lua`: - ![mem_ALLOC_SIZE](test/mem_ALLOC_SIZE.png) --- diff --git a/README_CN.md b/README_CN.md index 4a2983d..e088810 100644 --- a/README_CN.md +++ b/README_CN.md @@ -12,39 +12,24 @@ Lua 性能及内存分析工具 类似于 `gperftools`,**pLua** 可以分析 Lua 程序的热点函数及内存分配情况。 ## 特性 -- **简单**:只需几行代码即可开启采样输出结果,或者通过 [hookso](https://github.com/esrrhs/hookso) 动态注入到目标进程,无需修改源码。 -- **准确**:相比常规 Lua hook,定时采样的方式(基于 `ITIMER_PROF`)更能准确捕获 Lua 执行的 CPU 热点,并自动剔除 sleep 等空闲堆栈。 -- **轻量**:基于采样机制,对宿主程序的运行性能影响极小。 -- **直观**:输出调用关系图,兼容 `gperftools` 的 `pprof` 工具,可直接生成 [火焰图](https://github.com/brendangregg/FlameGraph)。 +- **简单**:只需几行代码即可开启采样。在 Linux 上还可通过 [hookso](https://github.com/esrrhs/hookso) 动态注入。 +- **准确**:POSIX 上使用 `ITIMER_PROF` / `SIGPROF` 定时采样;Windows(及其他可选场景)回退到 `LUA_MASKCOUNT`。 +- **多 State**:每个 `lua_State` 独立会话,可并行采样。**多 State ≠ 多 OS 线程安全**。 +- **CPU / 内存独立开关**:可同时开启,不再共用一个 `gRunning`。 +- **轻量 / 直观**:采样开销小,兼容 `pprof`,可生成火焰图。 ---- +## 平台 -## 依赖安装 +| 平台 | 产物 | CPU 后端 | hookso | 说明 | +|------|------|----------|--------|------| +| Linux | `bin/libplua.so` | SIGPROF(默认) | 支持 | 主平台 | +| macOS | `bin/libplua.so` | SIGPROF(默认) | **不支持** | 嵌入可用 | +| Windows | `bin/libplua.dll` | MASKCOUNT | **不支持** | 嵌入可用 | -pLua 依赖系统中的 Lua 开发库及头文件(支持 Lua 5.1/5.2/5.3/5.4 及 LuaJIT)。请使用包管理器自行安装依赖: +### Lua 版本 +支持 **Lua 5.2 / 5.3 / 5.4**。当前构建**不支持 Lua 5.1 / LuaJIT**(旧文档中的兼容声明不准确)。 -### Ubuntu / Debian -```shell -sudo apt-get update -sudo apt-get install -y lua5.3 liblua5.3-dev -``` - -### CentOS / RHEL / Fedora -```shell -# CentOS / RHEL (EPEL) -sudo yum install -y lua-devel -``` - -### macOS (Homebrew) -```shell -brew install lua -``` - ---- - -## 编译构建 - -使用 CMake 进行标准构建: +## 编译 ```shell mkdir build && cd build @@ -52,142 +37,28 @@ cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . ``` -编译生成的动态链接库位于 `bin/libplua.so`。如果环境已安装 Go,`tools/plua.go` 解析转换工具也会自动编译到 `bin/plua`。 - ---- +## 使用 -## 单元测试 - -使用 CTest 运行自动化单元测试: - -```shell -cd build -ctest --output-on-failure -``` - -或者直接运行 Lua 单元测试脚本: - -```shell -cd test -lua test_unit.lua -``` - ---- - -## 使用说明 - -### 1. 获取 CPU 采样数据 - -#### 方式一:在 Lua 代码中嵌入 ```lua --- 引入 libplua.so -local p = require "libplua" - --- 开启采样 --- 参数 1:采样时间(秒),0 表示一直采样直到显式停止 --- 参数 2:采样结果输出文件路径 -p.start(0, "call.pro") - -do_some_thing() +package.cpath = "./bin/?.so;./bin/?.dylib;./bin/?.dll;" .. package.cpath +local p = require "libplua" -- 也可在库名为 plua.so 时 require "plua" --- 结束采样,输出结果文件 +p.start(0, "call.pro") -- 可选第 3 参:间隔毫秒 +-- ... p.stop() -``` - -#### 方式二:使用 [hookso](https://github.com/esrrhs/hookso) 注入 -```shell -# a) 获取进程中的 lua_State 指针(例如进程的 xxx.so 调用了 lua_settop(L) 函数,则取第 1 个参数) -./hookso arg $PID xxx.so lua_settop 1 -123456 -# b) 加载 libplua.so -./hookso dlopen $PID ./libplua.so - -# c) 执行 libplua.so 的 lrealstart 手动开启,等价于 lrealstart(L, 0, "./call.pro") -./hookso call $PID libplua.so lrealstart i=123456 i=0 s="./call.pro" - -# d) 执行 libplua.so 的 lrealstop 手动关闭 -./hookso call $PID libplua.so lrealstop i=123456 -``` - ---- - -### 2. 获取内存采样数据 - -#### 方式一:在 Lua 代码中嵌入 -```lua --- 引入 libplua.so -local p = require "libplua" - --- 开启内存采样 --- 参数 1:采样时间(秒),0 表示一直采样 --- 参数 2:采样结果文件名前缀,会生成 2 个采样文件,分别代表内存分配大小(_ALLOC_SIZE.pro)与常驻内存占用大小(_USAGE.pro) -p.start_mem(0, "mem.pro") - -do_some_thing() - --- 如需查看常驻内存占用,可在 stop 前执行垃圾回收 +p.start_mem(0, "mem.pro") -- 可选第 3 参:采样字节率 collectgarbage("collect") - --- 结束采样,输出结果文件 p.stop_mem() -``` - -#### 方式二:使用 [hookso](https://github.com/esrrhs/hookso) 注入 -```shell -# a) 获取进程中的 lua_State 指针 -./hookso arg $PID xxx.so lua_settop 1 -123456 -# b) 加载 libplua.so -./hookso dlopen $PID ./libplua.so - -# c) 执行 libplua.so 的 lrealstartmem 手动开启 -./hookso call $PID libplua.so lrealstartmem i=123456 i=0 s="./mem.pro" - -# d) 执行 libplua.so 的 lrealstopmem 手动关闭 -./hookso call $PID libplua.so lrealstopmem i=123456 -``` - ---- - -## 可视化分析工具 - -`tools/` 目录提供了将 `.pro` 采样数据转换为火焰图(SVG)及调用图(DOT/PNG)的脚本。 - -### 可选依赖 -- **FlameGraph**:若本地及 PATH 中无 `flamegraph.pl`,`show.sh` 会尝试自动下载。CentOS 下可能需要 `yum install perl-open`。 -- **Graphviz**(用于生成调用关系 PNG 图片):`sudo apt install graphviz` / `sudo yum install graphviz`。 -- **pprof**(用于生成 DOT 调用图):`go install github.com/google/pprof@latest`(Ubuntu ≤24.04:`sudo apt install google-perftools`;RHEL:`sudo yum install gperftools`)。 - -### 批量生成可视化图表 -```shell -cd tools -./show.sh ../test +p.set_config({ cpu_interval_ms = 10, mem_rate = 524288, cpu_backend = "sigprof" }) ``` -输出产物包含: -- `.fl`:折叠堆栈(Folded stack)文件 -- `.svg`:交互式 SVG 火焰图 -- `.prof`:pprof 符号化分析数据 -- `.dot`:Graphviz 调用图源码 -- `.png`:调用图 PNG 图片(在安装了 graphviz 和 pprof 时自动渲染) - ---- - -## 示例 - -### CPU 调用热点图 -由 `test/test_cpu.lua` 采样生成: - -![call](test/call.png) - -### 内存分配图 -由 `test/test_mem.lua` 采样生成: +环境变量:`PLUA_CPU_INTERVAL_MS`、`PLUA_CPU_HOOK_COUNT`、`PLUA_MEM_RATE`、`PLUA_CPU_BACKEND`。 -![mem_ALLOC_SIZE](test/mem_ALLOC_SIZE.png) +`.pro` 文件带 `PLUA` magic + version;`tools/plua` 兼容旧格式。 ---- +hookso 注入仅 Linux;macOS / Windows 请使用嵌入方式。 ## 相关项目 - [lua-family-bucket](https://github.com/esrrhs/lua-family-bucket) From 52d3beb5604898998009df858d0046cf37c06dc1 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Tue, 3 Nov 2026 20:33:38 +0800 Subject: [PATCH 6/6] ci(windows): build Lua 5.4 from GitHub mirror for MSVC Chocolatey lua is 5.1.5 and lua.org FTP times out on GHA. Fetch lua/lua v5.4.7, compile lua54.dll/lib with cl, point CMake at them. --- .github/workflows/ccpp.yml | 90 ++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml index ff923f2..c8ed2fa 100644 --- a/.github/workflows/ccpp.yml +++ b/.github/workflows/ccpp.yml @@ -73,43 +73,87 @@ jobs: with: go-version: '1.22' - - name: Install Lua (via chocolatey / vcpkg fallback) + # Chocolatey's `lua` package is LuaForWindows 5.1.5 (too old; pLua needs 5.2+), + # and lua.org FTP often times out on GHA. Build Lua 5.4 from the GitHub mirror. + - name: Build Lua 5.4 (MSVC DLL + import lib) shell: pwsh run: | - choco install lua -y --no-progress - # Headers: download Lua source for includes if needed - if (-not (Test-Path "C:\Lua\include\lua.h")) { - Invoke-WebRequest -Uri "https://www.lua.org/ftp/lua-5.4.7.tar.gz" -OutFile "$env:RUNNER_TEMP\lua.tgz" - tar -xzf "$env:RUNNER_TEMP\lua.tgz" -C "$env:RUNNER_TEMP" - New-Item -ItemType Directory -Force -Path "C:\Lua\include" | Out-Null - Copy-Item "$env:RUNNER_TEMP\lua-5.4.7\src\*.h" "C:\Lua\include\" + $ErrorActionPreference = 'Stop' + $ver = '5.4.7' + $zip = Join-Path $env:RUNNER_TEMP "lua-$ver.zip" + $url = "https://github.com/lua/lua/archive/refs/tags/v$ver.zip" + $prefix = Join-Path $env:RUNNER_TEMP "lua-prefix" + Write-Host "Downloading $url" + Invoke-WebRequest -Uri $url -OutFile $zip + Expand-Archive -Path $zip -DestinationPath $env:RUNNER_TEMP -Force + $src = Join-Path $env:RUNNER_TEMP "lua-$ver" + if (-not (Test-Path (Join-Path $src "lua.h"))) { + throw "Lua sources not found under $src" } + New-Item -ItemType Directory -Force -Path "$prefix\include", "$prefix\lib", "$prefix\bin" | Out-Null + Copy-Item @( + "$src\lua.h", + "$src\luaconf.h", + "$src\lualib.h", + "$src\lauxlib.h" + ) -Destination "$prefix\include\" + if (Test-Path "$src\lua.hpp") { + Copy-Item "$src\lua.hpp" "$prefix\include\" + } + + $vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" + $vs = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $vs) { throw "Visual Studio with MSVC tools not found" } + $vcvars = Join-Path $vs "VC\Auxiliary\Build\vcvars64.bat" + if (-not (Test-Path $vcvars)) { throw "vcvars64.bat not found at $vcvars" } + + $bat = Join-Path $env:RUNNER_TEMP "build_lua54.bat" + @" + @echo on + call "$vcvars" || exit /b 1 + cd /d "$src" || exit /b 1 + cl /nologo /MD /O2 /W3 /c /DLUA_BUILD_AS_DLL l*.c + if errorlevel 1 exit /b 1 + del /q lua.obj luac.obj 2>nul + link /nologo /DLL /OUT:lua54.dll /IMPLIB:lua54.lib *.obj + if errorlevel 1 exit /b 1 + "@ | Set-Content -Path $bat -Encoding ASCII + + cmd /c $bat + if ($LASTEXITCODE -ne 0) { throw "MSVC Lua 5.4 build failed with exit $LASTEXITCODE" } + + Copy-Item "$src\lua54.dll" "$prefix\bin\" + Copy-Item "$src\lua54.lib" "$prefix\lib\" + if (-not (Test-Path "$prefix\lib\lua54.lib")) { throw "lua54.lib missing after build" } + + "LUA_PREFIX=$prefix" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "LUA_INCLUDE_DIR=$prefix\include" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "LUA_LIBRARY=$prefix\lib\lua54.lib" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "Lua 5.4 installed under $prefix" + Get-ChildItem -Recurse $prefix | Format-Table FullName, Length + - name: Configure CMake shell: pwsh run: | - $inc = "C:\ProgramData\chocolatey\lib\lua\tools\include" - if (-not (Test-Path $inc)) { $inc = "C:\Lua\include" } - $lib = "C:\ProgramData\chocolatey\lib\lua\tools" + $ErrorActionPreference = 'Stop' cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF ` - "-DLUA_INCLUDE_DIR=$inc" ` - "-DLUA_LIBRARIES=$lib\lua54.lib" ` - 2>$null - if ($LASTEXITCODE -ne 0) { - # Fallback: try FindLua defaults after choco - cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF - } + "-DLUA_INCLUDE_DIR=$env:LUA_INCLUDE_DIR" ` + "-DLUA_LIBRARY=$env:LUA_LIBRARY" ` + "-DLUA_LIBRARIES=$env:LUA_LIBRARY" + if ($LASTEXITCODE -ne 0) { throw "CMake configure failed" } - name: Build shell: pwsh run: | + $ErrorActionPreference = 'Stop' cmake --build build --config Release - if ($LASTEXITCODE -ne 0) { - Write-Host "Windows build failed — CI records the failure; see README for MSVC+Lua setup notes." - exit 1 - } + if ($LASTEXITCODE -ne 0) { throw "Windows build failed" } - name: Check DLL artifact shell: pwsh run: | - Get-ChildItem -Recurse bin, build -Filter "*.dll" -ErrorAction SilentlyContinue | Format-Table FullName, Length + $ErrorActionPreference = 'Stop' + $dlls = @(Get-ChildItem -Recurse bin, build -Filter "libplua.dll" -ErrorAction SilentlyContinue) + $dlls | Format-Table FullName, Length + if ($dlls.Count -lt 1) { throw "libplua.dll not produced" }