render function fixes, http optimizations

This commit is contained in:
Sky Johnson 2025-05-23 22:51:29 -05:00
parent ca85e735b0
commit 9f71fd7c0f
3 changed files with 176 additions and 127 deletions

View File

@ -2,6 +2,7 @@ package http
import ( import (
"context" "context"
"sync"
"time" "time"
"Moonshark/routers" "Moonshark/routers"
@ -26,6 +27,7 @@ type Server struct {
config *config.Config config *config.Config
sessionManager *sessions.SessionManager sessionManager *sessions.SessionManager
errorConfig utils.ErrorPageConfig errorConfig utils.ErrorPageConfig
ctxPool sync.Pool
} }
// New creates a new HTTP server // New creates a new HTTP server
@ -45,6 +47,11 @@ func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter,
OverrideDir: overrideDir, OverrideDir: overrideDir,
DebugMode: debugMode, DebugMode: debugMode,
}, },
ctxPool: sync.Pool{
New: func() any {
return make(map[string]any, 8)
},
},
} }
server.fasthttpServer = &fasthttp.Server{ server.fasthttpServer = &fasthttp.Server{
@ -78,53 +85,49 @@ func (s *Server) Shutdown(ctx context.Context) error {
// handleRequest processes the HTTP request // handleRequest processes the HTTP request
func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) { func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) {
start := time.Now() start := time.Now()
method := string(ctx.Method()) methodBytes := ctx.Method()
path := string(ctx.Path()) pathBytes := ctx.Path()
// Only convert to string once
method := string(methodBytes)
path := string(pathBytes)
if s.debugMode && path == "/debug/stats" { if s.debugMode && path == "/debug/stats" {
s.handleDebugStats(ctx) s.handleDebugStats(ctx)
if s.loggingEnabled { if s.loggingEnabled {
duration := time.Since(start) LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start))
LogRequest(ctx.Response.StatusCode(), method, path, duration)
} }
return return
} }
s.processRequest(ctx) s.processRequest(ctx, method, path)
if s.loggingEnabled { if s.loggingEnabled {
duration := time.Since(start) LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start))
LogRequest(ctx.Response.StatusCode(), method, path, duration)
} }
} }
// In server.go, modify the processRequest method // In server.go, modify the processRequest method
func (s *Server) processRequest(ctx *fasthttp.RequestCtx) { func (s *Server) processRequest(ctx *fasthttp.RequestCtx, method, path string) {
method := string(ctx.Method())
path := string(ctx.Path())
logger.Debug("Processing request %s %s", method, path) logger.Debug("Processing request %s %s", method, path)
params := &routers.Params{} params := &routers.Params{}
bytecode, scriptPath, routeErr, found := s.luaRouter.GetRouteInfo(method, path, params) bytecode, scriptPath, routeErr, found := s.luaRouter.GetRouteInfo(method, path, params)
// Check if we found a route but it has an error or no valid bytecode
if found && (len(bytecode) == 0 || routeErr != nil) { if found && (len(bytecode) == 0 || routeErr != nil) {
errorMsg := "Route exists but failed to compile. Check server logs for details." errorMsg := "Route exists but failed to compile. Check server logs for details."
if routeErr != nil { if routeErr != nil {
errorMsg = routeErr.Error() errorMsg = routeErr.Error()
} }
logger.Error("%s %s - %s", method, path, errorMsg) logger.Error("%s %s - %s", method, path, errorMsg)
ctx.SetContentType("text/html; charset=utf-8") ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusInternalServerError) ctx.SetStatusCode(fasthttp.StatusInternalServerError)
errorHTML := utils.InternalErrorPage(s.errorConfig, path, errorMsg) ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, errorMsg)))
ctx.SetBody([]byte(errorHTML))
return return
} else if found { }
if found {
logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count) logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count)
s.handleLuaRoute(ctx, bytecode, scriptPath, params, method, path) s.handleLuaRoute(ctx, bytecode, scriptPath, params, method, path)
return return
@ -143,58 +146,62 @@ func (s *Server) processRequest(ctx *fasthttp.RequestCtx) {
} }
// handleLuaRoute executes a Lua route // handleLuaRoute executes a Lua route
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method string, path string) { func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method, path string) {
// Create context for Lua execution
luaCtx := runner.NewHTTPContext(ctx) luaCtx := runner.NewHTTPContext(ctx)
defer luaCtx.Release() defer luaCtx.Release()
host := string(ctx.Host()) // Get pooled map for session data
sessionMap := s.ctxPool.Get().(map[string]any)
defer func() {
// Clear and return to pool
for k := range sessionMap {
delete(sessionMap, k)
}
s.ctxPool.Put(sessionMap)
}()
session := s.sessionManager.GetSessionFromRequest(ctx)
sessionMap["id"] = session.ID
sessionMap["data"] = session.Data
luaCtx.Set("method", method) luaCtx.Set("method", method)
luaCtx.Set("path", path) luaCtx.Set("path", path)
luaCtx.Set("host", host) luaCtx.Set("host", string(ctx.Host())) // Only convert when needed
luaCtx.Set("session", sessionMap)
session := s.sessionManager.GetSessionFromRequest(ctx)
luaCtx.Set("session", map[string]any{
"id": session.ID,
"data": session.Data,
})
// Optimize params handling
if params.Count > 0 { if params.Count > 0 {
paramMap := make(map[string]any, params.Count) paramMap := make(map[string]any, params.Count) // Pre-size
for i, key := range params.Keys { for i, key := range params.Keys {
paramMap[key] = params.Values[i] paramMap[key] = params.Values[i]
} }
luaCtx.Set("params", paramMap) luaCtx.Set("params", paramMap)
} else { } else {
luaCtx.Set("params", make(map[string]any)) luaCtx.Set("params", emptyMap) // Reuse empty map
} }
// Optimize form handling for POST methods
if method == "POST" || method == "PUT" || method == "PATCH" { if method == "POST" || method == "PUT" || method == "PATCH" {
formData, err := ParseForm(ctx) if formData, err := ParseForm(ctx); err == nil {
if err == nil && len(formData) > 0 {
luaCtx.Set("form", formData) luaCtx.Set("form", formData)
} else if err != nil {
logger.Warning("Error parsing form: %v", err)
luaCtx.Set("form", make(map[string]any))
} else { } else {
luaCtx.Set("form", make(map[string]any)) logger.Warning("Error parsing form: %v", err)
luaCtx.Set("form", emptyMap)
} }
} else { } else {
luaCtx.Set("form", make(map[string]any)) luaCtx.Set("form", emptyMap)
} }
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath) response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
if err != nil { if err != nil {
logger.Error("Error executing Lua route: %v", err) logger.Error("Error executing Lua route: %v", err)
ctx.SetContentType("text/html; charset=utf-8") ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusInternalServerError) ctx.SetStatusCode(fasthttp.StatusInternalServerError)
errorHTML := utils.InternalErrorPage(s.errorConfig, path, err.Error()) ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, err.Error())))
ctx.SetBody([]byte(errorHTML))
return return
} }
// Session handling optimization
if _, clearAll := response.SessionData["__clear_all"]; clearAll { if _, clearAll := response.SessionData["__clear_all"]; clearAll {
session.Clear() session.Clear()
delete(response.SessionData, "__clear_all") delete(response.SessionData, "__clear_all")
@ -207,8 +214,8 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
session.Set(k, v) session.Set(k, v)
} }
} }
s.sessionManager.ApplySessionCookie(ctx, session)
s.sessionManager.ApplySessionCookie(ctx, session)
runner.ApplyResponse(response, ctx) runner.ApplyResponse(response, ctx)
runner.ReleaseResponse(response) runner.ReleaseResponse(response)
} }

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"mime/multipart" "mime/multipart"
"strings" "strings"
"sync"
"time" "time"
"Moonshark/utils/logger" "Moonshark/utils/logger"
@ -13,63 +14,91 @@ import (
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
var emptyMap = make(map[string]any)
var (
stringPool = sync.Pool{
New: func() any {
return make([]string, 0, 4)
},
}
formDataPool = sync.Pool{
New: func() any {
return make(map[string]any, 16)
},
}
)
// LogRequest logs an HTTP request with its status code and duration // LogRequest logs an HTTP request with its status code and duration
func LogRequest(statusCode int, method, path string, duration time.Duration) { func LogRequest(statusCode int, method, path string, duration time.Duration) {
var statusColor, resetColor, methodColor string var statusColor, methodColor string
if statusCode >= 200 && statusCode < 300 { // Simplified color assignment
switch {
case statusCode < 300:
statusColor = "\u001b[32m" // Green for 2xx statusColor = "\u001b[32m" // Green for 2xx
} else if statusCode >= 300 && statusCode < 400 { case statusCode < 400:
statusColor = "\u001b[36m" // Cyan for 3xx statusColor = "\u001b[36m" // Cyan for 3xx
} else if statusCode >= 400 && statusCode < 500 { case statusCode < 500:
statusColor = "\u001b[33m" // Yellow for 4xx statusColor = "\u001b[33m" // Yellow for 4xx
} else { default:
statusColor = "\u001b[31m" // Red for 5xx and others statusColor = "\u001b[31m" // Red for 5xx+
} }
switch method { switch method {
case "GET": case "GET":
methodColor = "\u001b[32m" // Green methodColor = "\u001b[32m"
case "POST": case "POST":
methodColor = "\u001b[34m" // Blue methodColor = "\u001b[34m"
case "PUT": case "PUT":
methodColor = "\u001b[33m" // Yellow methodColor = "\u001b[33m"
case "DELETE": case "DELETE":
methodColor = "\u001b[31m" // Red methodColor = "\u001b[31m"
default: default:
methodColor = "\u001b[35m" // Magenta for others methodColor = "\u001b[35m"
} }
resetColor = "\u001b[0m" // Optimized duration formatting
var durationStr string var durationStr string
if duration.Milliseconds() < 1 { micros := duration.Microseconds()
durationStr = fmt.Sprintf("%.2fµs", float64(duration.Microseconds())) if micros < 1000 {
} else if duration.Milliseconds() < 1000 { durationStr = fmt.Sprintf("%.0fµs", float64(micros))
durationStr = fmt.Sprintf("%.2fms", float64(duration.Microseconds())/1000) } else if micros < 1000000 {
durationStr = fmt.Sprintf("%.1fms", float64(micros)/1000)
} else { } else {
durationStr = fmt.Sprintf("%.2fs", duration.Seconds()) durationStr = fmt.Sprintf("%.2fs", duration.Seconds())
} }
logger.Server("%s%d%s %s%s%s %s %s", logger.Server("%s%d\u001b[0m %s%s\u001b[0m %s %s",
statusColor, statusCode, resetColor, statusColor, statusCode,
methodColor, method, resetColor, methodColor, method,
path, durationStr) path, durationStr)
} }
// QueryToLua converts HTTP query args to a Lua-friendly map // QueryToLua converts HTTP query args to a Lua-friendly map
func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any { func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any {
queryMap := make(map[string]any) args := ctx.QueryArgs()
if args.Len() == 0 {
return emptyMap
}
ctx.QueryArgs().VisitAll(func(key, value []byte) { queryMap := make(map[string]any, args.Len()) // Pre-size
args.VisitAll(func(key, value []byte) {
k := string(key) k := string(key)
v := string(value) v := string(value)
if existing, ok := queryMap[k]; ok { if existing, exists := queryMap[k]; exists {
if arr, ok := existing.([]string); ok { // Handle multiple values more efficiently
queryMap[k] = append(arr, v) switch typed := existing.(type) {
} else if str, ok := existing.(string); ok { case []string:
queryMap[k] = []string{str, v} queryMap[k] = append(typed, v)
case string:
// Get slice from pool
slice := stringPool.Get().([]string)
slice = slice[:0] // Reset length
slice = append(slice, typed, v)
queryMap[k] = slice
} }
} else { } else {
queryMap[k] = v queryMap[k] = v
@ -81,21 +110,36 @@ func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any {
// ParseForm extracts form data from a request // ParseForm extracts form data from a request
func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) { func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
formData := make(map[string]any) contentType := string(ctx.Request.Header.ContentType())
if strings.Contains(string(ctx.Request.Header.ContentType()), "multipart/form-data") { if strings.Contains(contentType, "multipart/form-data") {
return parseMultipartForm(ctx) return parseMultipartForm(ctx)
} }
ctx.PostArgs().VisitAll(func(key, value []byte) { args := ctx.PostArgs()
if args.Len() == 0 {
return emptyMap, nil
}
formData := formDataPool.Get().(map[string]any)
// Clear the map (should already be clean from pool)
for k := range formData {
delete(formData, k)
}
args.VisitAll(func(key, value []byte) {
k := string(key) k := string(key)
v := string(value) v := string(value)
if existing, ok := formData[k]; ok { if existing, exists := formData[k]; exists {
if arr, ok := existing.([]string); ok { switch typed := existing.(type) {
formData[k] = append(arr, v) case []string:
} else if str, ok := existing.(string); ok { formData[k] = append(typed, v)
formData[k] = []string{str, v} case string:
slice := stringPool.Get().([]string)
slice = slice[:0]
slice = append(slice, typed, v)
formData[k] = slice
} }
} else { } else {
formData[k] = v formData[k] = v
@ -107,36 +151,43 @@ func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
// parseMultipartForm handles multipart/form-data requests // parseMultipartForm handles multipart/form-data requests
func parseMultipartForm(ctx *fasthttp.RequestCtx) (map[string]any, error) { func parseMultipartForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
formData := make(map[string]any)
form, err := ctx.MultipartForm() form, err := ctx.MultipartForm()
if err != nil { if err != nil {
return nil, err return nil, err
} }
formData := formDataPool.Get().(map[string]any)
for k := range formData {
delete(formData, k)
}
// Process form values
for key, values := range form.Value { for key, values := range form.Value {
if len(values) == 1 { switch len(values) {
case 0:
// Skip empty
case 1:
formData[key] = values[0] formData[key] = values[0]
} else if len(values) > 1 { default:
formData[key] = values formData[key] = values
} }
} }
// Process files if present
if len(form.File) > 0 { if len(form.File) > 0 {
files := make(map[string]any) files := make(map[string]any, len(form.File))
for fieldName, fileHeaders := range form.File { for fieldName, fileHeaders := range form.File {
if len(fileHeaders) == 1 { switch len(fileHeaders) {
case 1:
files[fieldName] = fileInfoToMap(fileHeaders[0]) files[fieldName] = fileInfoToMap(fileHeaders[0])
} else if len(fileHeaders) > 1 { default:
fileInfos := make([]map[string]any, 0, len(fileHeaders)) fileInfos := make([]map[string]any, len(fileHeaders))
for _, fh := range fileHeaders { for i, fh := range fileHeaders {
fileInfos = append(fileInfos, fileInfoToMap(fh)) fileInfos[i] = fileInfoToMap(fh)
} }
files[fieldName] = fileInfos files[fieldName] = fileInfos
} }
} }
formData["_files"] = files formData["_files"] = files
} }

View File

@ -438,17 +438,14 @@ _G.render = function(template_str, env)
local pos, chunks = 1, {} local pos, chunks = 1, {}
while pos <= #template_str do while pos <= #template_str do
local escaped_start = template_str:find("<%?", pos, true) local unescaped_start = template_str:find("{{{", pos, true)
local unescaped_start = template_str:find("<!", pos, true) local escaped_start = template_str:find("{{", pos, true)
local code_start = template_str:find("<%", pos, true)
local start, tag_type, open_len local start, tag_type, open_len
if escaped_start and (not unescaped_start or escaped_start < unescaped_start) and (not code_start or escaped_start < code_start) then if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
start, tag_type, open_len = unescaped_start, "-", 3
elseif escaped_start then
start, tag_type, open_len = escaped_start, "=", 2 start, tag_type, open_len = escaped_start, "=", 2
elseif unescaped_start and (not code_start or unescaped_start < code_start) then
start, tag_type, open_len = unescaped_start, "-", 2
elseif code_start then
start, tag_type, open_len = code_start, "code", 2
else else
table.insert(chunks, template_str:sub(pos)) table.insert(chunks, template_str:sub(pos))
break break
@ -459,25 +456,19 @@ _G.render = function(template_str, env)
end end
pos = start + open_len pos = start + open_len
local close_tag = tag_type == "=" and "?>" or tag_type == "-" and "!>" or "%>" local close_tag = tag_type == "-" and "}}}" or "}}"
local close_start, close_stop = template_str:find(close_tag, pos, true) local close_start, close_stop = template_str:find(close_tag, pos, true)
if not close_start then if not close_start then
error("Failed to find closing tag at position " .. pos) error("Failed to find closing tag at position " .. pos)
end end
local trim_newline = false local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$")
if template_str:sub(close_start-1, close_start-1) == "-" then
close_start = close_start - 1
trim_newline = true
end
local code = template_str:sub(pos, close_start-1) -- Check if it's a simple variable name for escaped output
table.insert(chunks, {tag_type, code, pos}) local is_simple_var = tag_type == "=" and code:match("^[%w_]+$")
table.insert(chunks, {tag_type, code, pos, is_simple_var})
pos = close_stop + 1 pos = close_stop + 1
if trim_newline and template_str:sub(pos, pos) == "\n" then
pos = pos + 1
end
end end
local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"} local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"}
@ -488,16 +479,16 @@ _G.render = function(template_str, env)
table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n") table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n")
else else
t = chunk[1] t = chunk[1]
if t == "code" then
table.insert(buffer, "--[[" .. chunk[3] .. "]] " .. chunk[2] .. "\n")
elseif t == "=" or t == "-" then
table.insert(buffer, "_b_i = _b_i + 1\n")
table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = ")
if t == "=" then if t == "=" then
table.insert(buffer, "_escape(_tostring(" .. chunk[2] .. "))\n") if chunk[4] then -- is_simple_var
table.insert(buffer, "_b_i = _b_i + 1\n")
table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _escape(_tostring(" .. chunk[2] .. "))\n")
else else
table.insert(buffer, "_tostring(" .. chunk[2] .. ")\n") table.insert(buffer, "--[[" .. chunk[3] .. "]] " .. chunk[2] .. "\n")
end end
elseif t == "-" then
table.insert(buffer, "_b_i = _b_i + 1\n")
table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _tostring(" .. chunk[2] .. ")\n")
end end
end end
end end
@ -521,14 +512,14 @@ _G.parse = function(template_str, env)
env = env or {} env = env or {}
while pos <= #template_str do while pos <= #template_str do
local escaped_start, escaped_end, escaped_name = template_str:find("<%?%s*([%w_]+)%s*%?>", pos) local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos)
local unescaped_start, unescaped_end, unescaped_name = template_str:find("<!%s*([%w_]+)%s*!>", pos) local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos)
local next_pos, placeholder_end, name, escaped local next_pos, placeholder_end, name, escaped
if escaped_start and (not unescaped_start or escaped_start < unescaped_start) then if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true
elseif unescaped_start then
next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false
elseif escaped_start then
next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true
else else
local text = template_str:sub(pos) local text = template_str:sub(pos)
if text and #text > 0 then if text and #text > 0 then
@ -561,14 +552,14 @@ _G.iparse = function(template_str, values)
values = values or {} values = values or {}
while pos <= #template_str do while pos <= #template_str do
local escaped_start, escaped_end = template_str:find("<%?>", pos, true) local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true)
local unescaped_start, unescaped_end = template_str:find("<!>", pos, true) local escaped_start, escaped_end = template_str:find("{{}}", pos, true)
local next_pos, placeholder_end, escaped local next_pos, placeholder_end, escaped
if escaped_start and (not unescaped_start or escaped_start < unescaped_start) then if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
next_pos, placeholder_end, escaped = escaped_start, escaped_end, true
elseif unescaped_start then
next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false
elseif escaped_start then
next_pos, placeholder_end, escaped = escaped_start, escaped_end, true
else else
local text = template_str:sub(pos) local text = template_str:sub(pos)
if text and #text > 0 then if text and #text > 0 then