commit ba4e9c3b61c151220eeb857e13743e1d10074cbd Author: Bruce Hill Date: Tue Mar 5 15:04:48 2019 -0800 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b692a92 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +MIT License +Copyright 2019 Bruce Hill + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3a82a9c --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +# makefile for sampleprof library for Lua + +LUA_DIR=/usr/local +LUA_INC= $(LUA_DIR)/include +LUA_BIN= $(LUA_DIR)/bin +LUA= lua + +CC= gcc +CFLAGS= $(INCS) $(WARN) -O3 $G +WARN= -std=c11 -pedantic -Wall -Wextra +INCS= -I$(LUA_INC) +#MAKESO= $(CC) $(CFLAGS) -shared +MAKESO= $(CC) $(CFLAGS) -bundle -undefined dynamic_lookup + +MYNAME= sampleprof +MYLIB= l$(MYNAME) +T= $(MYNAME).so +OBJS= $(MYLIB).o +TEST= test.lua + +all: $T + +.PHONY: test +test: $T + $(LUA_BIN)/$(LUA) $(TEST) + +o: $(MYLIB).o + +so: $T + +$T: $(OBJS) + $(MAKESO) -o $@ $(OBJS) + +$(OBJS): $(MYLIB).c + +clean: + rm -f $(OBJS) $T + +# eof diff --git a/README.md b/README.md new file mode 100644 index 0000000..e84e7d5 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# sampleprof - Lua Sample Profiling +Sample profiling is a code profiling technique where you randomly stop a +program and inspect its current state to learn about it. This approach is +faster and less disruptive than observing every line of code as it runs, since +observation has overhead. With a reasonable sample size, it's easy to get a +clear picture of which parts of the codebase are taking up most of the time, +while allowing the code to run almost unaffected. + +## Usage +The Lua library returns a single function, which takes a function to run, +performs sample profiling on it, and gives a table of results. The results +table is a mapping from `"filename.lua:line"` to number of samples. By default, +samples are propagated up the callstack, with a decay (0.619). +```lua +local sample = require('sampleprof') +local profile = sample(fn) +``` diff --git a/lsampleprof.c b/lsampleprof.c new file mode 100644 index 0000000..f6b5c3f --- /dev/null +++ b/lsampleprof.c @@ -0,0 +1,128 @@ +/* +* lsampleprof.c +* A Lua sample profiling library by Bruce Hill. This library returns a single +* function of the form profile([backprop=.619, [rate=100]], fn, ...) +* that runs `fn` with any extra args, performs sample profiling, and +* returns a table of the results. +* +* - `rate` the average number of samples per millisecond +* - `backprop` controls how weights are propagated up the callstack. 0 means "only tally +* the current line", 1 means "tally the current line and everything above it in the +* callstack equally", 0.5 means "tally each line with half weight of the thing below +* it in the callstack" +* +* The results table is a mapping from "file:line" -> number of times a random sample landed on that line +*/ + +#include "lua.h" +#include "lauxlib.h" + +#include +#include +#include +#include +#include +#include + +#define DEFAULT_BACKPROP 0.619 +#define DEFAULT_RATE 100 + +static lua_State *lastL; +static lua_Number backprop = DEFAULT_BACKPROP; +static lua_Number rate = DEFAULT_RATE; + +static inline void randomalarm() +{ + // Exponential decay: + int r = rand(); + int delay = -log((double)r/(double)RAND_MAX) * 1e3 / rate; + ualarm(1 + delay, 0); +} + +static inline void bump(lua_State *L, lua_Debug *ar, lua_Number amount) +{ + int profile_index = lua_gettop(L); + lua_getinfo(L, "lS", ar); + lua_pushfstring(L, "%s:%d", ar->short_src, ar->currentline); + int key_index = lua_gettop(L); + lua_pushvalue(L, key_index); + int type = lua_gettable(L, profile_index); + if (type == LUA_TNIL) { + lua_pop(L, 1); + lua_pushnumber(L, amount); + } else { + lua_Number count = lua_tonumber(L, -1); + count += amount; + lua_pop(L, 1); + lua_pushnumber(L, count); + } + lua_settable(L, profile_index); +} + +static void ltakesample(lua_State *L, lua_Debug *ar) +{ + lua_pushlightuserdata(L, &lastL); + lua_gettable(L, LUA_REGISTRYINDEX); + + lua_Number weight = 1.0; + bump(L, ar, weight); + lua_Debug ar2; + for (int i = 0; lua_getstack(L, i, &ar2); i++) { + weight *= backprop; + bump(L, &ar2, weight); + } + // Disable hook and set alarm + lua_sethook(lastL, NULL, 0, 0); + randomalarm(); +} + +static void handler(int sig) +{ + (void)sig; + lua_sethook(lastL, ltakesample, LUA_MASKLINE, 0); +} + +static int Lwrap(lua_State *L) +{ + // Backprop + if (lua_type(L, 1) == LUA_TNUMBER) { + backprop = lua_tonumber(L, 1); + lua_remove(L, 1); + } + // Sampling rate + if (lua_type(L, 1) == LUA_TNUMBER) { + rate = lua_tonumber(L, 1); + lua_remove(L, 1); + } + + struct sigaction newaction, oldaction; + memset(&newaction, sizeof(newaction), 0); + newaction.sa_handler = handler; + newaction.sa_flags = SA_NODEFER; + sigaction(SIGALRM, &newaction, &oldaction); + randomalarm(); + + lastL = L; + lua_call(L, lua_gettop(L)-1, 0); + lua_sethook(L, NULL, 0, 0); + + alarm(0); + //sigaction(SIGALRM, &oldaction, NULL); + + backprop = DEFAULT_BACKPROP; + rate = DEFAULT_RATE; + + lua_pushlightuserdata(L, &lastL); + lua_gettable(L, LUA_REGISTRYINDEX); + + return 1; +} + +LUALIB_API int luaopen_sampleprof(lua_State *L) +{ + lua_pushlightuserdata(L, &lastL); + lua_createtable(L, 0, 128); + lua_settable(L, LUA_REGISTRYINDEX); + lua_pushcfunction(L, Lwrap); + return 1; +} diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..83ab11c --- /dev/null +++ b/test.lua @@ -0,0 +1,56 @@ +local sample = require('sampleprof') + +local foo = function() + for i=1,1000 do + i = i + 1 + end +end + +local baz = function() + for i=1,500 do + i = i + 1 + end +end + +collectgarbage('stop') +local profile = sample(1,100,function(n) + assert(n == 23) + for i=1,1000 do + foo() + baz() + end +end, 23) + +assert(profile) + +-- Print a heatmap of the results: +if arg[1] == '-p' then + local f = io.open('test.lua') + local maxline = 0 + local lines = {} + for i=1,9999 do + local line = f:read("l") + if not line then break end + lines[i] = line + maxline = math.max(#line, maxline) + end + + local max, total = 0, 0 + for k,v in pairs(profile) do + local filename = k:match("([^:]*):") + if filename == 'test.lua' then + max = math.max(max, v) + total = total + v + end + end + + for i, line in ipairs(lines) do + local count = (profile[("test.lua:%d"):format(i)] or 0) + local percentmax = count/(max+1) + local k = math.floor(6*percentmax^.25) + local r,g,b = k+math.min(k,5-k),5-k+math.min(k,5-k),0 + if count == 0 then r,g,b = 2,2,1 end + local color = 16+36*r+6*g+b + print(("\x1b[2m%3d\x1b[0m \x1b[38;5;%dm%s%s\x1b[0m"):format(i, color, line, (" "):rep(maxline-#line))) + end +end