From 9cf03a5bcda81835d7ea8d28e69290b821a29d93 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 30 Jul 2026 08:12:34 +0800 Subject: [PATCH 1/2] build: isolate package backend sessions --- cl/caller_tracking_precompute_test.go | 79 ++++++++++ cl/instr.go | 56 ++++++- internal/build/backend_program_test.go | 89 +++++++++++ internal/build/build.go | 204 ++++++++++++++++++++++--- 4 files changed, 408 insertions(+), 20 deletions(-) create mode 100644 cl/caller_tracking_precompute_test.go create mode 100644 internal/build/backend_program_test.go diff --git a/cl/caller_tracking_precompute_test.go b/cl/caller_tracking_precompute_test.go new file mode 100644 index 0000000000..921d0a1af9 --- /dev/null +++ b/cl/caller_tracking_precompute_test.go @@ -0,0 +1,79 @@ +//go:build !llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cl + +import ( + "sync" + "testing" + + gossa "golang.org/x/tools/go/ssa" +) + +func TestCallerTrackingPrecomputeFreezesConcurrentReads(t *testing.T) { + var nilTracking *CallerTracking + nilTracking.Precompute(nil) + dep, root := buildCallerFrameSSAProgram(t, + "example.com/dep", `package dep +import "runtime" +func Where() { runtime.Caller(0) } +`, + "example.com/root", `package root +import "example.com/dep" +func Logs() { dep.Where() } +`) + tracking := NewCallerTracking() + tracking.Precompute([]*gossa.Package{root}) + tracking.Precompute(nil) + if !tracking.frozen { + t.Fatal("CallerTracking was not frozen after precomputation") + } + if !runtimeCallerBaseSet(tracking, dep)[dep.Func("Where")] { + t.Fatal("precomputed base set lost runtime caller function") + } + if !runtimeCallerFuncSet(tracking, root)[root.Func("Logs")] { + t.Fatal("precomputed extended set lost cross-package caller") + } + + var wg sync.WaitGroup + errs := make(chan struct{}, 32) + for range 32 { + wg.Add(1) + go func() { + defer wg.Done() + if !runtimeCallerBaseSet(tracking, dep)[dep.Func("Where")] || + !runtimeCallerFuncSet(tracking, root)[root.Func("Logs")] { + errs <- struct{}{} + } + }() + } + wg.Wait() + close(errs) + if len(errs) != 0 { + t.Fatal("concurrent read lost precomputed caller tracking data") + } + + delete(tracking.base, dep) + if got := runtimeCallerBaseSet(tracking, dep); got != nil { + t.Fatalf("frozen base lookup for unknown package = %v, want nil", got) + } + delete(tracking.extended, root) + if got := runtimeCallerFuncSet(tracking, root); got != nil { + t.Fatalf("frozen extended lookup for unknown package = %v, want nil", got) + } +} diff --git a/cl/instr.go b/cl/instr.go index 4b52624f1b..0f5a04a5a5 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -25,6 +25,7 @@ import ( "log" "os" "regexp" + "sort" "strings" "golang.org/x/tools/go/ssa" @@ -927,6 +928,9 @@ func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function if set, ok := c.extended[pkg]; ok { return set } + if c.frozen { + return nil + } base := runtimeCallerBaseSet(c, pkg) out := make(map[*ssa.Function]bool, len(base)) for fn := range base { @@ -980,12 +984,55 @@ func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function // queries (criterion 2 below) hit the memoization. It must not outlive // the compilation — the maps are keyed by *ssa.Package with // *ssa.Function values, so anything longer-lived would pin every -// compiled package's go/types and go/ssa graphs. Plain maps are enough: -// packages of one compilation are compiled sequentially (the LLVM -// context is not thread-safe). +// compiled package's go/types and go/ssa graphs. Concurrent drivers call +// Precompute and share only the resulting frozen, read-only maps. type CallerTracking struct { base map[*ssa.Package]map[*ssa.Function]bool extended map[*ssa.Package]map[*ssa.Function]bool + frozen bool +} + +// Precompute resolves every caller-tracking query before backend workers +// start, then freezes the maps for concurrent read-only access. +func (c *CallerTracking) Precompute(pkgs []*ssa.Package) { + if c == nil || c.frozen { + return + } + all := make(map[*ssa.Package]bool) + for _, pkg := range pkgs { + if pkg == nil { + continue + } + all[pkg] = true + if pkg.Prog != nil { + for _, programPkg := range pkg.Prog.AllPackages() { + if programPkg != nil { + all[programPkg] = true + } + } + } + } + ordered := make([]*ssa.Package, 0, len(all)) + for pkg := range all { + ordered = append(ordered, pkg) + } + sort.Slice(ordered, func(i, j int) bool { + left, right := "", "" + if ordered[i].Pkg != nil { + left = ordered[i].Pkg.Path() + } + if ordered[j].Pkg != nil { + right = ordered[j].Pkg.Path() + } + return left < right + }) + for _, pkg := range ordered { + runtimeCallerBaseSet(c, pkg) + } + for _, pkg := range ordered { + runtimeCallerFuncSet(c, pkg) + } + c.frozen = true } // NewCallerTracking creates the caller-tracking memoization for one @@ -1015,6 +1062,9 @@ func runtimeCallerBaseSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function if set, ok := c.base[pkg]; ok { return set } + if c.frozen { + return nil + } set := computeRuntimeCallerBaseSet(pkg) c.base[pkg] = set return set diff --git a/internal/build/backend_program_test.go b/internal/build/backend_program_test.go new file mode 100644 index 0000000000..dfa9bb125f --- /dev/null +++ b/internal/build/backend_program_test.go @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package build + +import ( + "go/types" + "testing" + + "github.com/goplus/llgo/cl" + "github.com/goplus/llgo/internal/packages" + llssa "github.com/goplus/llgo/ssa" +) + +func TestBackendProgramTemplateCreatesIsolatedSessions(t *testing.T) { + conf := &Config{Goos: "linux", Goarch: "amd64"} + template := newBackendProgramTemplate( + &llssa.Target{GOOS: conf.Goos, GOARCH: conf.Goarch}, + conf, + true, + true, + ) + first, err := template.newSession() + if err != nil { + t.Fatal(err) + } + defer first.prog.Dispose() + second, err := template.newSession() + if err != nil { + t.Fatal(err) + } + defer second.prog.Dispose() + if first.transformer == nil || second.transformer == nil { + t.Fatal("backend session missing C ABI transformer") + } + if !first.prog.FuncInfoMetadataEnabled() || !first.prog.FuncInfoSitesEnabled() { + t.Fatal("backend template did not preserve funcinfo configuration") + } + firstModule := first.prog.NewPackage("first", "example.com/first").Module() + secondModule := second.prog.NewPackage("second", "example.com/second").Module() + if firstModule.Context().C == secondModule.Context().C { + t.Fatal("backend sessions share an LLVM context") + } +} + +func TestBackendProgramTemplateOptionalState(t *testing.T) { + conf := &Config{Goos: "linux", Goarch: "amd64", DisableBoundsChecks: true, PthreadStackSize: 4096} + template := newBackendProgramTemplate(nil, conf, false, false) + template.typeSizes = &types.StdSizes{WordSize: 8, MaxAlign: 8} + template.runtimePackage = types.NewPackage(llssa.PkgRuntime, "runtime") + template.pythonPackage = types.NewPackage(llssa.PkgPython, "python") + prog := template.newProgram() + defer prog.Dispose() + if prog.Target() == nil { + t.Fatal("backend program created without a default target") + } + + validTypes := types.NewPackage("example.com/valid", "valid") + valid := &packages.Package{PkgPath: "example.com/valid", Types: validTypes} + duplicate := &packages.Package{PkgPath: "example.com/duplicate", Types: validTypes} + missingTypes := &packages.Package{PkgPath: "example.com/missing"} + illTyped := &packages.Package{PkgPath: "example.com/ill", Types: types.NewPackage("example.com/ill", "ill"), IllTyped: true} + inputs := collectBackendProgramInputs([]*packages.Package{missingTypes, illTyped, valid, duplicate}) + if len(inputs) != 1 || inputs[0].pkg != validTypes { + t.Fatalf("backend inputs = %#v, want one deduplicated package", inputs) + } + + patched := backendProgramTemplate{} + appendPatchedBackendInputs(&patched, cl.Patches{ + "example.com/missing": {}, + "example.com/other": {}, + }, packages.NewDeduper()) + if len(patched.inputs) != 0 { + t.Fatalf("missing patched packages produced inputs: %#v", patched.inputs) + } +} diff --git a/internal/build/build.go b/internal/build/build.go index 411d733568..e7efe6d792 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -452,8 +452,14 @@ func Build(inv Invocation) ([]Package, error) { llssa.Initialize(llssa.InitAll) }) - prog := llssa.NewProgram(target) - prog.DisableBoundsChecks(conf.DisableBoundsChecks) + funcInfo := conf.Mode != ModeGen && conf.PCLNMode != PCLNNone + backendTemplate := newBackendProgramTemplate( + target, + conf, + funcInfo, + shouldEnablePCLNSites(conf, funcInfo, emitDebugInfo), + ) + prog := backendTemplate.newProgram() if conf.Mode != ModeGen { // ModeGen callers (llgen and the golden suites) read LPkg.String() // after Do returns and dispose the program themselves; every other @@ -463,24 +469,17 @@ func Build(inv Invocation) ([]Package, error) { // harness) otherwise accumulate every compile's C++-side memory. defer prog.Dispose() } - prog.EnableGoGlobalDCE(conf.goGlobalDCEEnabled()) - prog.EnableDeadcodeDrop(conf.deadcodeDropEnabled()) - if conf.PthreadStackSize > 0 { - prog.SetPthreadStackSize(uint64(conf.PthreadStackSize)) - } - prog.EnableLTOPluginMarkers(conf.LTOPlugin.Enabled()) - funcInfo := conf.Mode != ModeGen && conf.PCLNMode != PCLNNone - prog.EnableFuncInfoMetadata(funcInfo) - // Site records are inline-asm fragments inside function bodies. Darwin - // DWARF builds avoid them because they disturb LLDB lexical scopes; Linux - // still needs them because its restricted dynamic symbol table cannot - // reconstruct every Go entry PC through dlsym. External mode always needs - // final-PC sites for sidecar construction. - prog.EnableFuncInfoSites(shouldEnablePCLNSites(conf, funcInfo, emitDebugInfo)) + var backendTypeSizes types.Sizes + var backendTypeSizesMu sync.Mutex sizes := func(sizes types.Sizes, compiler, arch string) types.Sizes { if arch == "wasm" { sizes = &types.StdSizes{WordSize: 4, MaxAlign: 4} } + backendTypeSizesMu.Lock() + if backendTypeSizes == nil { + backendTypeSizes = sizes + } + backendTypeSizesMu.Unlock() return prog.TypeSizes(sizes) } dedup := packages.NewDeduper() @@ -583,11 +582,22 @@ func Build(inv Invocation) ([]Package, error) { return altPkgs[0].Types }) prog.SetPython(func() *types.Package { - return dedup.Check(llssa.PkgPython).Types + if pkg := dedup.Check(llssa.PkgPython); pkg != nil { + return pkg.Types + } + return nil }) if err := prepareLocalVariables(prog, initial, altPkgs); err != nil { return nil, err } + backendTemplate.typeSizes = backendTypeSizes + backendTemplate.runtimePackage = altPkgs[0].Types + if pkg := dedup.Check(llssa.PkgPython); pkg != nil { + backendTemplate.pythonPackage = pkg.Types + } + backendTemplate.inputs = collectBackendProgramInputs(initial, altPkgs) + backendTemplate.llvmTarget = export.LLVMTarget + backendTemplate.targetABI = export.TargetABI buildMode := ssaBuildMode cabiOptimize := true @@ -602,9 +612,11 @@ func Build(inv Invocation) ([]Package, error) { if !IsOptimizeEnabled() { buildMode |= ssa.NaiveForm } + backendTemplate.cabiOptimize = cabiOptimize progSSA := ssa.NewProgram(initial[0].Fset, buildMode) patches := make(cl.Patches, len(altPkgPaths)) altEntries := registerAltSSAPkgs(progSSA, patches, altPkgs[1:], conf, verbose) + appendPatchedBackendInputs(&backendTemplate, patches, dedup) output := conf.OutFile != "" ctx := &context{conf: cfg, progSSA: progSSA, prog: prog, dedup: dedup, @@ -620,6 +632,7 @@ func Build(inv Invocation) ([]Package, error) { commands: commands, frontendOptions: frontendOptions, cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), + backend: backendTemplate, } defer ctx.closePackageMetas() defer ctx.closePackageArchiveBuffers() @@ -636,6 +649,7 @@ func Build(inv Invocation) ([]Package, error) { return nil, err } buildSSAPkgs(ctx, append(append(altEntries, pkgEntries...), depEntries...)) + ctx.callerTracking.Precompute(ctx.progSSA.AllPackages()) allPkgs := append([]*aPackage{}, pkgs...) allPkgs = append(allPkgs, depPkgs...) @@ -883,6 +897,7 @@ type context struct { frontendOptions cl.Options cTransformer *cabi.Transformer + backend backendProgramTemplate testFail bool @@ -915,6 +930,161 @@ func (c *context) closePackageMetas() { } } +type backendProgramInput struct { + fset *token.FileSet + pkg *types.Package + info *types.Info + files []*ast.File + parseSyntax bool + prepareLocal bool +} + +// backendProgramTemplate contains only immutable build-local inputs. Creating +// a session allocates a new llssa.Program, LLVM context, TargetMachine, and C +// ABI transformer; no LLVM-owned state is shared between sessions. +type backendProgramTemplate struct { + target *llssa.Target + disableBoundsChecks bool + typeSizes types.Sizes + goGlobalDCE bool + deadcodeDrop bool + pthreadStackSize int64 + ltoPluginMarkers bool + funcInfoMetadata bool + funcInfoSites bool + runtimePackage *types.Package + pythonPackage *types.Package + inputs []backendProgramInput + llvmTarget string + targetABI string + abiMode cabi.Mode + cabiOptimize bool +} + +type backendSession struct { + prog llssa.Program + transformer *cabi.Transformer +} + +func newBackendProgramTemplate(target *llssa.Target, conf *Config, funcInfoMetadata, funcInfoSites bool) backendProgramTemplate { + var targetCopy *llssa.Target + if target != nil { + copy := *target + targetCopy = © + } + return backendProgramTemplate{ + target: targetCopy, + disableBoundsChecks: conf.DisableBoundsChecks, + goGlobalDCE: conf.goGlobalDCEEnabled(), + deadcodeDrop: conf.deadcodeDropEnabled(), + pthreadStackSize: conf.PthreadStackSize, + ltoPluginMarkers: conf.LTOPlugin.Enabled(), + funcInfoMetadata: funcInfoMetadata, + funcInfoSites: funcInfoSites, + abiMode: conf.AbiMode, + } +} + +func (t backendProgramTemplate) newProgram() llssa.Program { + var target *llssa.Target + if t.target != nil { + copy := *t.target + target = © + } + prog := llssa.NewProgram(target) + prog.DisableBoundsChecks(t.disableBoundsChecks) + if t.typeSizes != nil { + prog.TypeSizes(t.typeSizes) + } + prog.EnableGoGlobalDCE(t.goGlobalDCE) + prog.EnableDeadcodeDrop(t.deadcodeDrop) + if t.pthreadStackSize > 0 { + prog.SetPthreadStackSize(uint64(t.pthreadStackSize)) + } + prog.EnableLTOPluginMarkers(t.ltoPluginMarkers) + prog.EnableFuncInfoMetadata(t.funcInfoMetadata) + prog.EnableFuncInfoSites(t.funcInfoSites) + if t.runtimePackage != nil { + prog.SetRuntime(t.runtimePackage) + } + if t.pythonPackage != nil { + prog.SetPython(t.pythonPackage) + } + return prog +} + +func (t backendProgramTemplate) newSession() (backendSession, error) { + prog := t.newProgram() + if err := t.replayProgramState(prog); err != nil { + prog.Dispose() + return backendSession{}, err + } + return backendSession{ + prog: prog, + transformer: cabi.NewTransformer(prog, t.llvmTarget, t.targetABI, t.abiMode, t.cabiOptimize), + }, nil +} + +func (t backendProgramTemplate) replayProgramState(prog llssa.Program) error { + for _, input := range t.inputs { + if input.parseSyntax { + if err := cl.ParsePkgSyntax(prog, input.fset, input.pkg, input.files); err != nil { + return err + } + } + } + for _, input := range t.inputs { + if input.prepareLocal { + if err := cl.PrepareLocalVariables(prog, input.fset, input.pkg, input.info, input.files); err != nil { + return err + } + } + } + return nil +} + +func collectBackendProgramInputs(groups ...[]*packages.Package) []backendProgramInput { + seen := make(map[*types.Package]bool) + var inputs []backendProgramInput + for _, roots := range groups { + packages.Visit(roots, nil, func(pkg *packages.Package) { + if pkg == nil || pkg.Types == nil || pkg.IllTyped || seen[pkg.Types] { + return + } + seen[pkg.Types] = true + inputs = append(inputs, backendProgramInput{ + fset: pkg.Fset, + pkg: pkg.Types, + info: pkg.TypesInfo, + files: slices.Clone(pkg.Syntax), + parseSyntax: !llruntime.SkipToBuild(pkg.PkgPath), + prepareLocal: true, + }) + }) + } + return inputs +} + +func appendPatchedBackendInputs(template *backendProgramTemplate, patches cl.Patches, dedup packages.Deduper) { + paths := make([]string, 0, len(patches)) + for pkgPath := range patches { + paths = append(paths, pkgPath) + } + slices.Sort(paths) + for _, pkgPath := range paths { + alt := dedup.Check(altPkgPathPrefix + pkgPath) + if alt == nil || len(alt.Syntax) == 0 { + continue + } + template.inputs = append(template.inputs, backendProgramInput{ + fset: alt.Fset, + pkg: types.NewPackage(pkgPath, ""), + files: slices.Clone(alt.Syntax), + parseSyntax: true, + }) + } +} + func (c *context) compiler() *clang.Cmd { config := clang.NewConfig( c.crossCompile.CC, From 5591d1668bca43a4b971f868cbe3550765f73bf2 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Fri, 31 Jul 2026 08:39:28 +0800 Subject: [PATCH 2/2] build: replay locality state in backend sessions --- internal/build/backend_program_test.go | 2 +- internal/build/build.go | 37 ++++++-------- ssa/locality.go | 69 ++++++++++++++++++++++++++ ssa/locality_test.go | 28 +++++++++++ 4 files changed, 114 insertions(+), 22 deletions(-) diff --git a/internal/build/backend_program_test.go b/internal/build/backend_program_test.go index dfa9bb125f..161d2aa2ae 100644 --- a/internal/build/backend_program_test.go +++ b/internal/build/backend_program_test.go @@ -73,7 +73,7 @@ func TestBackendProgramTemplateOptionalState(t *testing.T) { duplicate := &packages.Package{PkgPath: "example.com/duplicate", Types: validTypes} missingTypes := &packages.Package{PkgPath: "example.com/missing"} illTyped := &packages.Package{PkgPath: "example.com/ill", Types: types.NewPackage("example.com/ill", "ill"), IllTyped: true} - inputs := collectBackendProgramInputs([]*packages.Package{missingTypes, illTyped, valid, duplicate}) + inputs := collectBackendProgramInputs(prog, []*packages.Package{missingTypes, illTyped, valid, duplicate}) if len(inputs) != 1 || inputs[0].pkg != validTypes { t.Fatalf("backend inputs = %#v, want one deduplicated package", inputs) } diff --git a/internal/build/build.go b/internal/build/build.go index e7efe6d792..00986faf0c 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -595,7 +595,8 @@ func Build(inv Invocation) ([]Package, error) { if pkg := dedup.Check(llssa.PkgPython); pkg != nil { backendTemplate.pythonPackage = pkg.Types } - backendTemplate.inputs = collectBackendProgramInputs(initial, altPkgs) + backendTemplate.inputs = collectBackendProgramInputs(prog, initial, altPkgs) + backendTemplate.localities = prog.SnapshotLocalityState() backendTemplate.llvmTarget = export.LLVMTarget backendTemplate.targetABI = export.TargetABI @@ -931,12 +932,11 @@ func (c *context) closePackageMetas() { } type backendProgramInput struct { - fset *token.FileSet - pkg *types.Package - info *types.Info - files []*ast.File - parseSyntax bool - prepareLocal bool + fset *token.FileSet + pkg *types.Package + info *types.Info + files []*ast.File + parseSyntax bool } // backendProgramTemplate contains only immutable build-local inputs. Creating @@ -955,6 +955,7 @@ type backendProgramTemplate struct { runtimePackage *types.Package pythonPackage *types.Package inputs []backendProgramInput + localities llssa.LocalityState llvmTarget string targetABI string abiMode cabi.Mode @@ -1033,17 +1034,11 @@ func (t backendProgramTemplate) replayProgramState(prog llssa.Program) error { } } } - for _, input := range t.inputs { - if input.prepareLocal { - if err := cl.PrepareLocalVariables(prog, input.fset, input.pkg, input.info, input.files); err != nil { - return err - } - } - } + prog.RestoreLocalityState(t.localities) return nil } -func collectBackendProgramInputs(groups ...[]*packages.Package) []backendProgramInput { +func collectBackendProgramInputs(prog llssa.Program, groups ...[]*packages.Package) []backendProgramInput { seen := make(map[*types.Package]bool) var inputs []backendProgramInput for _, roots := range groups { @@ -1052,13 +1047,13 @@ func collectBackendProgramInputs(groups ...[]*packages.Package) []backendProgram return } seen[pkg.Types] = true + parsed := prog.PackageSyntaxParsed(pkg.Types) inputs = append(inputs, backendProgramInput{ - fset: pkg.Fset, - pkg: pkg.Types, - info: pkg.TypesInfo, - files: slices.Clone(pkg.Syntax), - parseSyntax: !llruntime.SkipToBuild(pkg.PkgPath), - prepareLocal: true, + fset: pkg.Fset, + pkg: pkg.Types, + info: pkg.TypesInfo, + files: slices.Clone(pkg.Syntax), + parseSyntax: parsed && !llruntime.SkipToBuild(pkg.PkgPath), }) }) } diff --git a/ssa/locality.go b/ssa/locality.go index 0e9b41e6cd..cd3402b773 100644 --- a/ssa/locality.go +++ b/ssa/locality.go @@ -61,6 +61,17 @@ type localityInfos struct { parsedPackages map[*types.Package]struct{} } +// LocalityState is a copyable snapshot of the Go-side locality metadata owned +// by one Program. It contains no LLVM objects and can therefore be replayed +// safely into an independent backend Program. +type LocalityState struct { + entries map[string]VariableLocality + ownerlessEntries map[string]VariableLocality + declarationEntries map[string]map[string]VariableLocality + activePackages map[string]struct{} + parsedPackages map[*types.Package]struct{} +} + func newLocalityInfos() *localityInfos { return &localityInfos{ entries: make(map[string]VariableLocality), @@ -71,6 +82,64 @@ func newLocalityInfos() *localityInfos { } } +// SnapshotLocalityState returns an independent snapshot of p's locality +// metadata. Callers may use it after preflight has completed to seed isolated +// backend sessions without sharing mutable Program state. +func (p Program) SnapshotLocalityState() LocalityState { + p.localities.mu.RLock() + defer p.localities.mu.RUnlock() + state := LocalityState{ + entries: cloneLocalityEntries(p.localities.entries), + ownerlessEntries: cloneLocalityEntries(p.localities.ownerlessEntries), + activePackages: cloneLocalitySet(p.localities.activePackages), + parsedPackages: cloneParsedPackages(p.localities.parsedPackages), + } + state.declarationEntries = make(map[string]map[string]VariableLocality, len(p.localities.declarationEntries)) + for name, entries := range p.localities.declarationEntries { + state.declarationEntries[name] = cloneLocalityEntries(entries) + } + return state +} + +// RestoreLocalityState replaces p's locality metadata with an independent +// copy of state. The copied maps keep backend sessions independent. +func (p Program) RestoreLocalityState(state LocalityState) { + p.localities.mu.Lock() + p.localities.entries = cloneLocalityEntries(state.entries) + p.localities.ownerlessEntries = cloneLocalityEntries(state.ownerlessEntries) + p.localities.activePackages = cloneLocalitySet(state.activePackages) + p.localities.parsedPackages = cloneParsedPackages(state.parsedPackages) + p.localities.declarationEntries = make(map[string]map[string]VariableLocality, len(state.declarationEntries)) + for name, entries := range state.declarationEntries { + p.localities.declarationEntries[name] = cloneLocalityEntries(entries) + } + p.localities.mu.Unlock() +} + +func cloneLocalityEntries(entries map[string]VariableLocality) map[string]VariableLocality { + ret := make(map[string]VariableLocality, len(entries)) + for name, entry := range entries { + ret[name] = entry + } + return ret +} + +func cloneLocalitySet(entries map[string]struct{}) map[string]struct{} { + ret := make(map[string]struct{}, len(entries)) + for name := range entries { + ret[name] = struct{}{} + } + return ret +} + +func cloneParsedPackages(entries map[*types.Package]struct{}) map[*types.Package]struct{} { + ret := make(map[*types.Package]struct{}, len(entries)) + for pkg := range entries { + ret[pkg] = struct{}{} + } + return ret +} + func (p *localityInfos) update(name string, update func(*VariableLocality)) { p.mu.Lock() info := p.entries[name] diff --git a/ssa/locality_test.go b/ssa/locality_test.go index 1d6ad63cb5..b2a79f0ad1 100644 --- a/ssa/locality_test.go +++ b/ssa/locality_test.go @@ -63,6 +63,34 @@ func TestLocalityInfos(t *testing.T) { } } +func TestLocalityStateSnapshotIsIndependent(t *testing.T) { + source := NewProgram(nil) + defer source.Dispose() + pkg := types.NewPackage("example.com/p", "p") + name := "example.com/p.state" + source.DeclareLocality(pkg, "state", LocalityInfo{Locality: GoroutineLocal, HasInitializer: true}) + source.SetLocalStorageFor(pkg, name, LocalStoragePackage) + source.ActivateLocalitiesFor(pkg) + source.MarkPackageSyntaxParsed(pkg) + + dest := NewProgram(nil) + defer dest.Dispose() + dest.RestoreLocalityState(source.SnapshotLocalityState()) + got, ok := dest.VariableLocalityFor(pkg, name) + if !ok || got.Locality != GoroutineLocal || got.LocalStorage != LocalStoragePackage { + t.Fatalf("restored locality = %+v, %v", got, ok) + } + if !dest.NeedsLocalContext() || !dest.PackageSyntaxParsed(pkg) { + t.Fatal("restored locality state lost active or parsed metadata") + } + + source.SetLocalStorageFor(pkg, name, LocalStorageNativeTLS) + got, ok = dest.VariableLocalityFor(pkg, name) + if !ok || got.LocalStorage != LocalStoragePackage { + t.Fatalf("restored locality aliased source state: %+v, %v", got, ok) + } +} + func TestPackageLocalitiesRetainDeclarationOwners(t *testing.T) { prog := NewProgram(nil) std := types.NewPackage("runtime", "runtime")