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

View File

@ -6,6 +6,7 @@ import (
"fmt"
"mime/multipart"
"strings"
"sync"
"time"
"Moonshark/utils/logger"
@ -13,63 +14,91 @@ import (
"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
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
} else if statusCode >= 300 && statusCode < 400 {
case statusCode < 400:
statusColor = "\u001b[36m" // Cyan for 3xx
} else if statusCode >= 400 && statusCode < 500 {
case statusCode < 500:
statusColor = "\u001b[33m" // Yellow for 4xx
} else {
statusColor = "\u001b[31m" // Red for 5xx and others
default:
statusColor = "\u001b[31m" // Red for 5xx+
}
switch method {
case "GET":
methodColor = "\u001b[32m" // Green
methodColor = "\u001b[32m"
case "POST":
methodColor = "\u001b[34m" // Blue
methodColor = "\u001b[34m"
case "PUT":
methodColor = "\u001b[33m" // Yellow
methodColor = "\u001b[33m"
case "DELETE":
methodColor = "\u001b[31m" // Red
methodColor = "\u001b[31m"
default:
methodColor = "\u001b[35m" // Magenta for others
methodColor = "\u001b[35m"
}
resetColor = "\u001b[0m"
// Optimized duration formatting
var durationStr string
if duration.Milliseconds() < 1 {
durationStr = fmt.Sprintf("%.2fµs", float64(duration.Microseconds()))
} else if duration.Milliseconds() < 1000 {
durationStr = fmt.Sprintf("%.2fms", float64(duration.Microseconds())/1000)
micros := duration.Microseconds()
if micros < 1000 {
durationStr = fmt.Sprintf("%.0fµs", float64(micros))
} else if micros < 1000000 {
durationStr = fmt.Sprintf("%.1fms", float64(micros)/1000)
} else {
durationStr = fmt.Sprintf("%.2fs", duration.Seconds())
}
logger.Server("%s%d%s %s%s%s %s %s",
statusColor, statusCode, resetColor,
methodColor, method, resetColor,
logger.Server("%s%d\u001b[0m %s%s\u001b[0m %s %s",
statusColor, statusCode,
methodColor, method,
path, durationStr)
}
// QueryToLua converts HTTP query args to a Lua-friendly map
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)
v := string(value)
if existing, ok := queryMap[k]; ok {
if arr, ok := existing.([]string); ok {
queryMap[k] = append(arr, v)
} else if str, ok := existing.(string); ok {
queryMap[k] = []string{str, v}
if existing, exists := queryMap[k]; exists {
// Handle multiple values more efficiently
switch typed := existing.(type) {
case []string:
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 {
queryMap[k] = v
@ -81,21 +110,36 @@ func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any {
// ParseForm extracts form data from a request
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)
}
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)
v := string(value)
if existing, ok := formData[k]; ok {
if arr, ok := existing.([]string); ok {
formData[k] = append(arr, v)
} else if str, ok := existing.(string); ok {
formData[k] = []string{str, v}
if existing, exists := formData[k]; exists {
switch typed := existing.(type) {
case []string:
formData[k] = append(typed, v)
case string:
slice := stringPool.Get().([]string)
slice = slice[:0]
slice = append(slice, typed, v)
formData[k] = slice
}
} else {
formData[k] = v
@ -107,36 +151,43 @@ func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
// parseMultipartForm handles multipart/form-data requests
func parseMultipartForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
formData := make(map[string]any)
form, err := ctx.MultipartForm()
if err != nil {
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 {
if len(values) == 1 {
switch len(values) {
case 0:
// Skip empty
case 1:
formData[key] = values[0]
} else if len(values) > 1 {
default:
formData[key] = values
}
}
// Process files if present
if len(form.File) > 0 {
files := make(map[string]any)
files := make(map[string]any, len(form.File))
for fieldName, fileHeaders := range form.File {
if len(fileHeaders) == 1 {
switch len(fileHeaders) {
case 1:
files[fieldName] = fileInfoToMap(fileHeaders[0])
} else if len(fileHeaders) > 1 {
fileInfos := make([]map[string]any, 0, len(fileHeaders))
for _, fh := range fileHeaders {
fileInfos = append(fileInfos, fileInfoToMap(fh))
default:
fileInfos := make([]map[string]any, len(fileHeaders))
for i, fh := range fileHeaders {
fileInfos[i] = fileInfoToMap(fh)
}
files[fieldName] = fileInfos
}
}
formData["_files"] = files
}

View File

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