mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-30 18:14:32 -04:00
The cached gallery listing refreshed each entry's installed flag with one os.Stat per entry, under the cache's global write lock. A gallery holds about 1,900 entries. On a models directory on SMB, one refresh took about 14s. The listing and every row's VRAM estimate run this refresh, and the lock serialized them, so the models page took minutes to load. The installed check now lists the models directory once and looks up each entry in that listing. On the same SMB share the listing takes about 75ms. The answers match os.Stat: a symlink counts only when its target exists, and names with a path separator still use os.Stat. The listing runs before the lock is taken, so the lock covers only the flag updates. Concurrent callers on a cold cache now share one upstream load. Before this, each caller fetched the gallery index and the configs itself. Assisted-by: Claude:claude-opus-5-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
62 lines
1.8 KiB
Go
62 lines
1.8 KiB
Go
package gallery
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const modelConfigExt = ".yaml"
|
|
|
|
// installedConfigs answers "does <modelsPath>/<name>.yaml exist?" for every
|
|
// entry of a gallery from a single read of the models directory.
|
|
//
|
|
// The question used to be asked with one os.Stat per gallery entry. The gallery
|
|
// holds thousands of entries and the models directory is often network storage
|
|
// (SMB, NFS), where each Stat is a round trip, so one listing cost seconds. The
|
|
// listing is read by the gallery page and by one VRAM estimate per row, which
|
|
// turned a page view into minutes.
|
|
//
|
|
// Answers match os.Stat on the same path: a symlink counts only when its target
|
|
// exists, and anything else carrying the name counts, directories included.
|
|
// Names that are not a plain file name are checked with os.Stat directly, since
|
|
// they point outside the listed directory.
|
|
func installedConfigs(modelsPath string) func(name string) bool {
|
|
statInstalled := func(name string) bool {
|
|
_, err := os.Stat(filepath.Join(modelsPath, name+modelConfigExt))
|
|
return err == nil
|
|
}
|
|
|
|
entries, err := os.ReadDir(modelsPath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return func(string) bool { return false }
|
|
}
|
|
// A directory that exists but cannot be listed may still answer a
|
|
// Stat, so fall back rather than report everything as not installed.
|
|
return statInstalled
|
|
}
|
|
|
|
present := make(map[string]struct{}, len(entries))
|
|
for _, e := range entries {
|
|
base, ok := strings.CutSuffix(e.Name(), modelConfigExt)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if e.Type()&fs.ModeSymlink != 0 && !statInstalled(base) {
|
|
continue
|
|
}
|
|
present[base] = struct{}{}
|
|
}
|
|
|
|
return func(name string) bool {
|
|
if strings.ContainsRune(name, '/') || strings.ContainsRune(name, filepath.Separator) {
|
|
return statInstalled(name)
|
|
}
|
|
_, ok := present[name]
|
|
return ok
|
|
}
|
|
}
|