Skip to content

Commit

Permalink
feat(tools/spxls): introduce compile cache
Browse files Browse the repository at this point in the history
Signed-off-by: Aofei Sheng <[email protected]>
  • Loading branch information
aofei committed Dec 26, 2024
1 parent b64a5ab commit c9edea1
Show file tree
Hide file tree
Showing 20 changed files with 346 additions and 370 deletions.
5 changes: 4 additions & 1 deletion spx-gui/src/components/editor/code-editor/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ export class SpxLSPClient extends Disposable {
if (['.spx', '.json'].includes(ext)) {
// Only `.spx` & `.json` files are needed for `spxls`
const ab = await file.arrayBuffer()
loadedFiles[path] = { content: new Uint8Array(ab) }
loadedFiles[path] = {
content: new Uint8Array(ab),
modTime: Date.now(), // FIXME: use file.lastModified
}
debugFiles[path] = await toText(file)
}
})
Expand Down
1 change: 1 addition & 0 deletions tools/spxls/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,5 @@ export type Files = {
*/
export type File = {
content: Uint8Array
modTime: number // unix timestamp in milliseconds
}
21 changes: 8 additions & 13 deletions tools/spxls/internal/server/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,26 @@ import (
"testing"

"github.com/goplus/builder/tools/spxls/internal/util"
"github.com/goplus/builder/tools/spxls/internal/vfs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestServerSpxGetDefinitions(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
s := New(vfs.NewMapFS(func() map[string][]byte {
return map[string][]byte{
"main.spx": []byte(`
s := New(newMapFSWithoutModTime(map[string][]byte{
"main.spx": []byte(`
var (
MySprite Sprite
)
MySprite.turn Left
run "assets", {Title: "My Game"}
`),
"MySprite.spx": []byte(`
"MySprite.spx": []byte(`
onStart => {
MySprite.turn Right
}
`),
"assets/sprites/MySprite/index.json": []byte(`{}`),
}
"assets/sprites/MySprite/index.json": []byte(`{}`),
}), nil)

mainSpxFileScopeParams := []SpxGetDefinitionsParams{
Expand Down Expand Up @@ -143,24 +140,22 @@ onStart => {
})

t.Run("TrailingEmptyLinesOfSpriteCode", func(t *testing.T) {
s := New(vfs.NewMapFS(func() map[string][]byte {
return map[string][]byte{
"main.spx": []byte(`
s := New(newMapFSWithoutModTime(map[string][]byte{
"main.spx": []byte(`
var (
MySprite Sprite
)
MySprite.turn Left
run "assets", {Title: "My Game"}
`),
"MySprite.spx": []byte(`
"MySprite.spx": []byte(`
onStart => {
MySprite.turn Right
}
`),
"assets/sprites/MySprite/index.json": []byte(`{}`),
}
"assets/sprites/MySprite/index.json": []byte(`{}`),
}), nil)

mainSpxFileScopeParams := []SpxGetDefinitionsParams{
Expand Down
69 changes: 69 additions & 0 deletions tools/spxls/internal/server/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"fmt"
"go/types"
"io/fs"
"maps"
"path"
"slices"
"strings"
"time"

"github.com/goplus/builder/tools/spxls/internal/util"
"github.com/goplus/builder/tools/spxls/internal/vfs"
Expand Down Expand Up @@ -302,6 +304,12 @@ func (r *compileResult) addDiagnostics(documentURI DocumentURI, diags ...Diagnos
r.diagnostics[documentURI] = append(r.diagnostics[documentURI], dedupedDiags...)
}

// compileCache represents a cache for compilation results.
type compileCache struct {
result *compileResult
spxFileModTimes map[string]time.Time
}

// compile compiles spx source files and returns compile result.
//
// TODO: Move diagnostics from [compileResult] to error return value by using
Expand All @@ -311,7 +319,67 @@ func (s *Server) compile() (*compileResult, error) {
if err != nil {
return nil, fmt.Errorf("failed to get spx files: %w", err)
}
if len(spxFiles) == 0 {
return nil, errNoValidSpxFiles
}

// Try to use cache first.
slices.Sort(spxFiles)
s.compileCacheMu.RLock()
if cache := s.lastCompileCache; cache != nil {
// Check if spx file set has changed.
cachedSpxFiles := slices.Sorted(maps.Keys(cache.spxFileModTimes))
if !slices.Equal(spxFiles, cachedSpxFiles) {
s.compileCacheMu.RUnlock()
} else {
// Check if any spx file has been modified.
modified := false
for _, file := range spxFiles {
fi, err := fs.Stat(s.workspaceRootFS, file)
if err != nil {
s.compileCacheMu.RUnlock()
return nil, fmt.Errorf("failed to stat file %q: %w", file, err)
}
if cachedModTime, ok := cache.spxFileModTimes[file]; !ok || !fi.ModTime().Equal(cachedModTime) {
modified = true
break
}
}
if !modified {
s.compileCacheMu.RUnlock()
return cache.result, nil
}
}
}
s.compileCacheMu.RUnlock()

// Compile uncached if cache is not used.
result, err := s.compileUncached(spxFiles)
if err != nil {
return nil, err
}

// Update cache.
modTimes := make(map[string]time.Time, len(spxFiles))
for _, spxFile := range spxFiles {
fi, err := fs.Stat(s.workspaceRootFS, spxFile)
if err != nil {
return nil, fmt.Errorf("failed to stat file %q: %w", spxFile, err)
}
modTimes[spxFile] = fi.ModTime()
}
s.compileCacheMu.Lock()
s.lastCompileCache = &compileCache{
result: result,
spxFileModTimes: modTimes,
}
s.compileCacheMu.Unlock()

return result, nil
}

// compileUncached compiles spx source files without using cache.
func (s *Server) compileUncached(spxFiles []string) (*compileResult, error) {
result := &compileResult{
fset: goptoken.NewFileSet(),
mainPkg: types.NewPackage("main", "main"),
Expand Down Expand Up @@ -422,6 +490,7 @@ func (s *Server) compile() (*compileResult, error) {
return result, nil
}

var err error
result.spxPkg, err = s.importer.Import(spxPkgPath)
if err != nil {
return nil, fmt.Errorf("failed to import spx package: %w", err)
Expand Down
49 changes: 17 additions & 32 deletions tools/spxls/internal/server/definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,26 @@ package server
import (
"testing"

"github.com/goplus/builder/tools/spxls/internal/vfs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestServerTextDocumentDefinition(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
s := New(vfs.NewMapFS(func() map[string][]byte {
return map[string][]byte{
"main.spx": []byte(`
s := New(newMapFSWithoutModTime(map[string][]byte{
"main.spx": []byte(`
var (
MySprite Sprite
)
MySprite.turn Left
run "assets", {Title: "My Game"}
`),
"MySprite.spx": []byte(`
"MySprite.spx": []byte(`
onStart => {
MySprite.turn Right
}
`),
"assets/sprites/MySprite/index.json": []byte(`{}`),
}
"assets/sprites/MySprite/index.json": []byte(`{}`),
}), nil)

mainSpxMySpriteDef, err := s.textDocumentDefinition(&DefinitionParams{
Expand Down Expand Up @@ -73,12 +70,10 @@ onStart => {
})

t.Run("BuiltinType", func(t *testing.T) {
s := New(vfs.NewMapFS(func() map[string][]byte {
return map[string][]byte{
"main.spx": []byte(`
s := New(newMapFSWithoutModTime(map[string][]byte{
"main.spx": []byte(`
var x int
`),
}
}), nil)

def, err := s.textDocumentDefinition(&DefinitionParams{
Expand All @@ -92,12 +87,10 @@ var x int
})

t.Run("InvalidPosition", func(t *testing.T) {
s := New(vfs.NewMapFS(func() map[string][]byte {
return map[string][]byte{
"main.spx": []byte(`
s := New(newMapFSWithoutModTime(map[string][]byte{
"main.spx": []byte(`
var x int
`),
}
}), nil)

def, err := s.textDocumentDefinition(&DefinitionParams{
Expand All @@ -113,15 +106,13 @@ var x int

func TestServerTextDocumentTypeDefinition(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
s := New(vfs.NewMapFS(func() map[string][]byte {
return map[string][]byte{
"main.spx": []byte(`
s := New(newMapFSWithoutModTime(map[string][]byte{
"main.spx": []byte(`
type MyType struct {
field int
}
var x MyType
`),
}
}), nil)

def, err := s.textDocumentTypeDefinition(&TypeDefinitionParams{
Expand All @@ -143,15 +134,13 @@ var x MyType
})

t.Run("SpriteType", func(t *testing.T) {
s := New(vfs.NewMapFS(func() map[string][]byte {
return map[string][]byte{
"main.spx": []byte(`
s := New(newMapFSWithoutModTime(map[string][]byte{
"main.spx": []byte(`
var (
MySprite Sprite
)
`),
"assets/sprites/MySprite/index.json": []byte(`{}`),
}
"assets/sprites/MySprite/index.json": []byte(`{}`),
}), nil)

def, err := s.textDocumentTypeDefinition(&TypeDefinitionParams{
Expand All @@ -165,12 +154,10 @@ var (
})

t.Run("BuiltinType", func(t *testing.T) {
s := New(vfs.NewMapFS(func() map[string][]byte {
return map[string][]byte{
"main.spx": []byte(`
s := New(newMapFSWithoutModTime(map[string][]byte{
"main.spx": []byte(`
var x int
`),
}
}), nil)

def, err := s.textDocumentTypeDefinition(&TypeDefinitionParams{
Expand All @@ -184,12 +171,10 @@ var x int
})

t.Run("InvalidPosition", func(t *testing.T) {
s := New(vfs.NewMapFS(func() map[string][]byte {
return map[string][]byte{
"main.spx": []byte(`
s := New(newMapFSWithoutModTime(map[string][]byte{
"main.spx": []byte(`
var x int
`),
}
}), nil)

def, err := s.textDocumentTypeDefinition(&TypeDefinitionParams{
Expand Down
Loading

0 comments on commit c9edea1

Please sign in to comment.