Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tools/spxls): introduce compile cache #1176

Merged
merged 1 commit into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
aofei marked this conversation as resolved.
Show resolved Hide resolved
// [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
Loading