Initial commit

This commit is contained in:
Bruce Hill 2019-03-05 15:04:48 -08:00
commit ba4e9c3b61
5 changed files with 260 additions and 0 deletions

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
MIT License
Copyright 2019 Bruce Hill <bruce@bruce-hill.com>
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.

39
Makefile Normal file
View File

@ -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

17
README.md Normal file
View File

@ -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)
```

128
lsampleprof.c Normal file
View File

@ -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 <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <math.h>
#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;
}

56
test.lua Normal file
View File

@ -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