diff --git a/routers/luaRouter.go b/routers/luaRouter.go index f29204f..f5c2e49 100644 --- a/routers/luaRouter.go +++ b/routers/luaRouter.go @@ -52,6 +52,15 @@ type LuaRouter struct { // Middleware tracking for path hierarchy middlewareFiles map[string][]string // path -> middleware file paths + + // New caching fields + middlewareCache map[string][]byte // path -> content + sourceCache map[string][]byte // combined source cache key -> compiled bytecode + sourceMtimes map[string]time.Time // track modification times + + // Shared Lua state for compilation + compileState *luajit.State + compileStateMu sync.Mutex // Protect concurrent access to Lua state } // node represents a node in the routing trie @@ -75,6 +84,12 @@ func NewLuaRouter(routesDir string) (*LuaRouter, error) { return nil, errors.New("routes path is not a directory") } + // Create shared Lua state + compileState := luajit.New() + if compileState == nil { + return nil, errors.New("failed to create Lua compile state") + } + r := &LuaRouter{ routesDir: routesDir, routes: make(map[string]*node), @@ -82,6 +97,10 @@ func NewLuaRouter(routesDir string) (*LuaRouter, error) { middlewareFiles: make(map[string][]string), routeCache: fastcache.New(defaultRouteMaxBytes), bytecodeCache: fastcache.New(defaultBytecodeMaxBytes), + middlewareCache: make(map[string][]byte), + sourceCache: make(map[string][]byte), + sourceMtimes: make(map[string]time.Time), + compileState: compileState, } methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} @@ -221,18 +240,86 @@ func (r *LuaRouter) compileWithMiddleware(n *node, urlPath, scriptPath string) e return nil } - // Collect middleware for this path (cascading from root) - middlewareChain := r.getMiddlewareChain(urlPath) + // Check if we need to recompile by comparing modification times + sourceKey := r.getSourceCacheKey(urlPath, scriptPath) + needsRecompile := false - // Read and combine all source files + // Check handler modification time + handlerInfo, err := os.Stat(scriptPath) + if err != nil { + n.err = err + return err + } + + lastCompiled, exists := r.sourceMtimes[sourceKey] + if !exists || handlerInfo.ModTime().After(lastCompiled) { + needsRecompile = true + } + + // Check middleware modification times + if !needsRecompile { + middlewareChain := r.getMiddlewareChain(urlPath) + for _, mwPath := range middlewareChain { + mwInfo, err := os.Stat(mwPath) + if err != nil { + n.err = err + return err + } + if mwInfo.ModTime().After(lastCompiled) { + needsRecompile = true + break + } + } + } + + // Use cached bytecode if available and fresh + if !needsRecompile { + if bytecode, exists := r.sourceCache[sourceKey]; exists { + bytecodeKey := getBytecodeKey(scriptPath) + r.bytecodeCache.Set(bytecodeKey, bytecode) + return nil + } + } + + // Build combined source + combinedSource, err := r.buildCombinedSource(urlPath, scriptPath) + if err != nil { + n.err = err + return err + } + + // Compile combined source using shared state + r.compileStateMu.Lock() + bytecode, err := r.compileState.CompileBytecode(combinedSource, scriptPath) + r.compileStateMu.Unlock() + + if err != nil { + n.err = err + return err + } + + // Cache everything + bytecodeKey := getBytecodeKey(scriptPath) + r.bytecodeCache.Set(bytecodeKey, bytecode) + r.sourceCache[sourceKey] = bytecode + r.sourceMtimes[sourceKey] = time.Now() + + n.err = nil + return nil +} + +// buildCombinedSource builds the combined middleware + handler source +func (r *LuaRouter) buildCombinedSource(urlPath, scriptPath string) (string, error) { var combinedSource strings.Builder + // Get middleware chain + middlewareChain := r.getMiddlewareChain(urlPath) + // Add middleware in order for _, mwPath := range middlewareChain { - content, err := os.ReadFile(mwPath) + content, err := r.getFileContent(mwPath) if err != nil { - n.err = err - return err + return "", err } combinedSource.WriteString("-- Middleware: ") combinedSource.WriteString(mwPath) @@ -242,36 +329,51 @@ func (r *LuaRouter) compileWithMiddleware(n *node, urlPath, scriptPath string) e } // Add main handler - content, err := os.ReadFile(scriptPath) + content, err := r.getFileContent(scriptPath) if err != nil { - n.err = err - return err + return "", err } combinedSource.WriteString("-- Handler: ") combinedSource.WriteString(scriptPath) combinedSource.WriteString("\n") combinedSource.Write(content) - // Compile combined source - state := luajit.New() - if state == nil { - compileErr := errors.New("failed to create Lua state") - n.err = compileErr - return compileErr - } - defer state.Close() + return combinedSource.String(), nil +} - bytecode, err := state.CompileBytecode(combinedSource.String(), scriptPath) +// getFileContent reads file content with caching +func (r *LuaRouter) getFileContent(path string) ([]byte, error) { + // Check cache first + if content, exists := r.middlewareCache[path]; exists { + // Verify file hasn't changed + info, err := os.Stat(path) + if err == nil { + if cachedTime, exists := r.sourceMtimes[path]; exists && !info.ModTime().After(cachedTime) { + return content, nil + } + } + } + + // Read from disk + content, err := os.ReadFile(path) if err != nil { - n.err = err - return err + return nil, err } - bytecodeKey := getBytecodeKey(scriptPath) - r.bytecodeCache.Set(bytecodeKey, bytecode) + // Cache it + r.middlewareCache[path] = content + r.sourceMtimes[path] = time.Now() - n.err = nil - return nil + return content, nil +} + +// getSourceCacheKey generates a unique key for combined source +func (r *LuaRouter) getSourceCacheKey(urlPath, scriptPath string) string { + middlewareChain := r.getMiddlewareChain(urlPath) + var keyParts []string + keyParts = append(keyParts, middlewareChain...) + keyParts = append(keyParts, scriptPath) + return strings.Join(keyParts, "|") } // getMiddlewareChain returns middleware files that apply to the given path @@ -564,6 +666,9 @@ func (r *LuaRouter) Refresh() error { r.failedRoutes = make(map[string]*RouteError) r.middlewareFiles = make(map[string][]string) + r.middlewareCache = make(map[string][]byte) + r.sourceCache = make(map[string][]byte) + r.sourceMtimes = make(map[string]time.Time) err := r.buildRoutes() @@ -587,10 +692,23 @@ func (r *LuaRouter) ReportFailedRoutes() []*RouteError { return result } -// ClearCache clears both the route and bytecode caches +// ClearCache clears all caches func (r *LuaRouter) ClearCache() { r.routeCache.Reset() r.bytecodeCache.Reset() + r.middlewareCache = make(map[string][]byte) + r.sourceCache = make(map[string][]byte) + r.sourceMtimes = make(map[string]time.Time) +} + +// Close cleans up the router and its resources +func (r *LuaRouter) Close() { + r.compileStateMu.Lock() + if r.compileState != nil { + r.compileState.Close() + r.compileState = nil + } + r.compileStateMu.Unlock() } // GetCacheStats returns statistics about the cache diff --git a/tests/basic_test.go b/tests/basic_test.go index bfb7d9e..38c446b 100644 --- a/tests/basic_test.go +++ b/tests/basic_test.go @@ -298,7 +298,7 @@ func BenchmarkRouteCompilation(b *testing.B) { routesDir := filepath.Join(tempDir, "routes") os.MkdirAll(routesDir, 0755) - for b.Loop() { + for i := 0; i < b.N; i++ { b.StopTimer() os.RemoveAll(routesDir) os.MkdirAll(routesDir, 0755)