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 <aofei@aofeisheng.com>
  • Loading branch information
aofei committed Dec 27, 2024
1 parent a6b1cef commit 54ccebc
Show file tree
Hide file tree
Showing 22 changed files with 362 additions and 386 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 @@ -62,7 +62,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: this.project.modTime ?? Date.now()
}
debugFiles[path] = await toText(file)
}
})
Expand Down
14 changes: 12 additions & 2 deletions spx-gui/src/models/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class Project extends Disposable {
releaseCount?: number
remixCount?: number

/** Files' hash of game content, available when project under editing */
/** Files' hash of game content, available when project is under editing */
filesHash?: string
private lastSyncedFilesHash?: string
/** If there is any change of game content not synced (to cloud) yet. */
Expand All @@ -114,6 +114,9 @@ export class Project extends Disposable {
return this.lastSyncedFilesHash !== this.filesHash
}

/** Modification time in milliseconds of project state, available when project is under editing */
modTime?: number

stage: Stage
sprites: Sprite[]
sounds: Sound[]
Expand Down Expand Up @@ -553,7 +556,12 @@ export class Project extends Disposable {
)
}

/** watch for all changes, auto save to local cache, or touch all files to trigger lazy loading to ensure they are in memory */
/**
* Watch for all changes to:
* 1. Auto save to local cache when enabled.
* 2. Touch all files to trigger lazy loading when not in local cache mode.
* 3. Update modification time.
*/
private autoSaveToLocalCache: (() => void) | null = null
private startAutoSaveToLocalCache(localCacheKey: string) {
const saveToLocalCache = debounce(() => this.saveToLocalCache(localCacheKey), 1000)
Expand All @@ -569,6 +577,7 @@ export class Project extends Disposable {
this.autoSaveToLocalCache = () => {
if (this.autoSaveMode === AutoSaveMode.LocalCache) saveToLocalCache()
else touchFiles()
this.modTime = Date.now()
}

this.addDisposer(
Expand All @@ -588,6 +597,7 @@ export class Project extends Disposable {
if (this.lastSyncedFilesHash == null) {
this.lastSyncedFilesHash = this.filesHash
}
this.modTime = Date.now()
this.startAutoSaveToCloud(localCacheKey)
this.startAutoSaveToLocalCache(localCacheKey)

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
}
27 changes: 10 additions & 17 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,14 +140,12 @@ onStart => {
})

t.Run("ParseError", 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(`
// Invalid syntax
var (
MySprite Sprite
`),
}
}), nil)

mainSpxFileScopeParams := []SpxGetDefinitionsParams{
Expand Down Expand Up @@ -183,24 +178,22 @@ var (
})

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
70 changes: 67 additions & 3 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,16 +304,77 @@ 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
// [errors.Join].
func (s *Server) compile() (*compileResult, error) {
spxFiles, err := s.spxFiles()
if err != nil {
return nil, fmt.Errorf("failed to get spx files: %w", err)
}
if len(spxFiles) == 0 {
return nil, errNoValidSpxFiles
}
slices.Sort(spxFiles)

s.compileCacheMu.Lock()
defer s.compileCacheMu.Unlock()

// Try to use cache first.
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) {
// Check if any spx file has been modified.
modified := false
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)
}
if cachedModTime, ok := cache.spxFileModTimes[spxFile]; !ok || !fi.ModTime().Equal(cachedModTime) {
modified = true
break
}
}
if !modified {
return cache.result, nil
}
}
}

// 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.lastCompileCache = &compileCache{
result: result,
spxFileModTimes: modTimes,
}

return result, nil
}

// compileUncached compiles spx source files without using cache.
//
// TODO: Move diagnostics from [compileResult] to error return value by using
// [errors.Join].
func (s *Server) compileUncached(spxFiles []string) (*compileResult, error) {
result := &compileResult{
fset: goptoken.NewFileSet(),
mainPkg: types.NewPackage("main", "main"),
Expand Down Expand Up @@ -419,6 +482,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
11 changes: 4 additions & 7 deletions tools/spxls/internal/server/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,27 @@ 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 TestServerTextDocumentCompletion(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.
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)

emptyLineItems, err := s.textDocumentCompletion(&CompletionParams{
Expand Down
Loading

0 comments on commit 54ccebc

Please sign in to comment.