Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview/pages: rework caching mechanism

instead of loading all templates at once and storing into a map, we now
memoize the results of `parse`. the first call to `parse` will require
calculation but subsequent calls will be cached.

this is simpler to reason about because the new execution model requires
us to parse differently for each "base" template that is being used:

- for timeline, it is necessary to parse with layouts/base
- for repo-index, it is necessary to parse with layouts/base and
layouts/repobase in that order

the previous approach to loading also had a latent bug: all layouts were
loaded atop each other in alphabetical order (order of iteration over
the filesystem), and therefore it was not possible to selectively parse
and execute templates on a subset of layouts.

Signed-off-by: oppiliappan <me@oppi.li>

authored by

oppiliappan and committed by
Tangled
a008c1e7 3977b53f

+133 -85
+35
appview/pages/cache.go
··· 1 + package pages 2 + 3 + import ( 4 + "sync" 5 + ) 6 + 7 + type TmplCache[K comparable, V any] struct { 8 + data map[K]V 9 + mutex sync.RWMutex 10 + } 11 + 12 + func NewTmplCache[K comparable, V any]() *TmplCache[K, V] { 13 + return &TmplCache[K, V]{ 14 + data: make(map[K]V), 15 + } 16 + } 17 + 18 + func (c *TmplCache[K, V]) Get(key K) (V, bool) { 19 + c.mutex.RLock() 20 + defer c.mutex.RUnlock() 21 + val, exists := c.data[key] 22 + return val, exists 23 + } 24 + 25 + func (c *TmplCache[K, V]) Set(key K, value V) { 26 + c.mutex.Lock() 27 + defer c.mutex.Unlock() 28 + c.data[key] = value 29 + } 30 + 31 + func (c *TmplCache[K, V]) Size() int { 32 + c.mutex.RLock() 33 + defer c.mutex.RUnlock() 34 + return len(c.data) 35 + }
+98 -85
appview/pages/pages.go
··· 42 42 var Files embed.FS 43 43 44 44 type Pages struct { 45 - mu sync.RWMutex 46 - t map[string]*template.Template 45 + mu sync.RWMutex 46 + cache *TmplCache[string, *template.Template] 47 47 48 48 avatar config.AvatarConfig 49 49 resolver *idresolver.Resolver ··· 65 65 66 66 p := &Pages{ 67 67 mu: sync.RWMutex{}, 68 - t: make(map[string]*template.Template), 68 + cache: NewTmplCache[string, *template.Template](), 69 69 dev: config.Core.Dev, 70 70 avatar: config.Avatar, 71 71 rctx: rctx, ··· 74 74 logger: slog.Default().With("component", "pages"), 75 75 } 76 76 77 - // Initial load of all templates 78 - p.loadAllTemplates() 77 + if p.dev { 78 + p.embedFS = os.DirFS(p.templateDir) 79 + } else { 80 + p.embedFS = Files 81 + } 79 82 80 83 return p 84 + } 85 + 86 + func (p *Pages) pathToName(s string) string { 87 + return strings.TrimSuffix(strings.TrimPrefix(s, "templates/"), ".html") 88 + } 89 + 90 + // reverse of pathToName 91 + func (p *Pages) nameToPath(s string) string { 92 + return "templates/" + s + ".html" 81 93 } 82 94 83 95 func (p *Pages) fragmentPaths() ([]string, error) { ··· 117 105 return fragmentPaths, nil 118 106 } 119 107 120 - func (p *Pages) loadAllTemplates() { 121 - if p.dev { 122 - p.embedFS = os.DirFS(p.templateDir) 123 - } else { 124 - p.embedFS = Files 125 - } 126 - 127 - l := p.logger.With("handler", "loadAllTemplates") 128 - templates := make(map[string]*template.Template) 108 + func (p *Pages) fragments() (*template.Template, error) { 129 109 fragmentPaths, err := p.fragmentPaths() 130 110 if err != nil { 131 - l.Error("failed to collect fragments", "err", err) 132 - return 111 + return nil, err 133 112 } 113 + 114 + funcs := p.funcMap() 134 115 135 116 // parse all fragments together 136 - allFragments := template.New("").Funcs(p.funcMap()) 117 + allFragments := template.New("").Funcs(funcs) 137 118 for _, f := range fragmentPaths { 138 - name := strings.TrimPrefix(f, "templates/") 139 - name = strings.TrimSuffix(name, ".html") 140 - pf, err := template.New(name).Funcs(p.funcMap()).ParseFS(p.embedFS, f) 119 + name := p.pathToName(f) 120 + 121 + pf, err := template.New(name). 122 + Funcs(funcs). 123 + ParseFS(p.embedFS, f) 141 124 if err != nil { 142 - l.Error("failed to parse fragment", "name", name, "path", f) 143 - return 125 + return nil, err 144 126 } 127 + 145 128 allFragments, err = allFragments.AddParseTree(name, pf.Tree) 146 129 if err != nil { 147 - l.Error("failed to add parse tree", "name", name, "path", f) 148 - return 130 + return nil, err 149 131 } 150 - templates[name] = allFragments.Lookup(name) 151 132 } 152 - // Then walk through and setup the rest of the templates 153 - err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 154 - if err != nil { 155 - return err 156 - } 157 - if d.IsDir() { 158 - return nil 159 - } 160 - if !strings.HasSuffix(path, "html") { 161 - return nil 162 - } 163 - // Skip fragments as they've already been loaded 164 - if strings.Contains(path, "fragments/") { 165 - return nil 166 - } 167 - // Skip layouts 168 - if strings.Contains(path, "layouts/") { 169 - return nil 170 - } 171 - name := strings.TrimPrefix(path, "templates/") 172 - name = strings.TrimSuffix(name, ".html") 173 - // Add the page template on top of the base 174 - allPaths := []string{} 175 - allPaths = append(allPaths, "templates/layouts/*.html") 176 - allPaths = append(allPaths, fragmentPaths...) 177 - allPaths = append(allPaths, path) 178 - tmpl, err := template.New(name). 179 - Funcs(p.funcMap()). 180 - ParseFS(p.embedFS, allPaths...) 181 - if err != nil { 182 - return fmt.Errorf("setting up template: %w", err) 183 - } 184 - templates[name] = tmpl 185 - l.Debug("loaded all templates") 186 - return nil 187 - }) 133 + 134 + return allFragments, nil 135 + } 136 + 137 + // parse without memoization 138 + func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 139 + paths, err := p.fragmentPaths() 188 140 if err != nil { 189 - l.Error("walking template dir", "err", err) 190 - panic(err) 141 + return nil, err 142 + } 143 + for _, s := range stack { 144 + paths = append(paths, p.nameToPath(s)) 191 145 } 192 146 193 - l.Info("loaded all templates", "total", len(templates)) 194 - p.mu.Lock() 195 - defer p.mu.Unlock() 196 - p.t = templates 147 + funcs := p.funcMap() 148 + top := stack[len(stack)-1] 149 + parsed, err := template.New(top). 150 + Funcs(funcs). 151 + ParseFS(p.embedFS, paths...) 152 + if err != nil { 153 + return nil, err 154 + } 155 + 156 + return parsed, nil 197 157 } 198 158 199 - func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error { 200 - // In dev mode, reparse templates from disk before executing 201 - if p.dev { 202 - p.loadAllTemplates() 159 + func (p *Pages) parse(stack ...string) (*template.Template, error) { 160 + key := strings.Join(stack, "|") 161 + 162 + // never cache in dev mode 163 + if cached, exists := p.cache.Get(key); !p.dev && exists { 164 + return cached, nil 203 165 } 204 166 205 - p.mu.RLock() 206 - defer p.mu.RUnlock() 207 - tmpl, exists := p.t[templateName] 208 - if !exists { 209 - return fmt.Errorf("template not found: %s", templateName) 167 + result, err := p.rawParse(stack...) 168 + if err != nil { 169 + return nil, err 210 170 } 211 171 212 - if base == "" { 213 - return tmpl.Execute(w, params) 214 - } else { 215 - return tmpl.ExecuteTemplate(w, base, params) 216 - } 172 + p.cache.Set(key, result) 173 + return result, nil 217 174 } 218 175 219 - func (p *Pages) execute(name string, w io.Writer, params any) error { 220 - return p.executeOrReload(name, w, "layouts/base", params) 176 + func (p *Pages) parseBase(top string) (*template.Template, error) { 177 + stack := []string{ 178 + "layouts/base", 179 + top, 180 + } 181 + return p.parse(stack...) 182 + } 183 + 184 + func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 185 + stack := []string{ 186 + "layouts/base", 187 + "layouts/repobase", 188 + top, 189 + } 190 + return p.parse(stack...) 221 191 } 222 192 223 193 func (p *Pages) executePlain(name string, w io.Writer, params any) error { 224 - return p.executeOrReload(name, w, "", params) 194 + tpl, err := p.parse(name) 195 + if err != nil { 196 + return err 197 + } 198 + 199 + return tpl.Execute(w, params) 200 + } 201 + 202 + func (p *Pages) execute(name string, w io.Writer, params any) error { 203 + tpl, err := p.parseBase(name) 204 + if err != nil { 205 + return err 206 + } 207 + 208 + return tpl.ExecuteTemplate(w, "layouts/base", params) 225 209 } 226 210 227 211 func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 228 - return p.executeOrReload(name, w, "layouts/repobase", params) 212 + tpl, err := p.parseRepoBase(name) 213 + if err != nil { 214 + return err 215 + } 216 + 217 + return tpl.ExecuteTemplate(w, "layouts/base", params) 229 218 } 230 219 231 220 func (p *Pages) Favicon(w io.Writer) error {