mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-03 02:27:48 +00:00
89d9e33bca
* interp: bail out of loops that iterate too many times The existing loop guard (errLoopUnrolled) only fires when a loop body emits runtime instructions. Loops that are fully evaluable at compile time, such as inserting thousands of entries into a map, were not caught and could hang the compiler. Add a per-basic-block iteration counter that triggers a recoverable error (errLoopTooLong) when any block is entered more than 1000 times in a single function call. This defers the init function to runtime, which is the same behavior as other interp bailouts. Profiling showed that 83% of CPU time was spent in GC, caused by allocation pressure from the interp memory cloning on each map mutation. The iteration limit avoids this entirely by bailing out before the quadratic cost becomes significant. Performance on the reproducer from #2090 (map init with strconv.Itoa): entries before after 5,000 7.4s 2.1s 10,000 17.5s 2.1s 20,000 48.0s 2.8s 65,536 >180s (OOM) 3.2s * Add -interp-loop-limit
126 lines
2.9 KiB
Go
126 lines
2.9 KiB
Go
package interp
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"tinygo.org/x/go-llvm"
|
|
)
|
|
|
|
func TestInterp(t *testing.T) {
|
|
for _, name := range []string{
|
|
"basic",
|
|
"phi",
|
|
"consteval",
|
|
"intrinsics",
|
|
"copy",
|
|
"interface",
|
|
"revert",
|
|
"alloc",
|
|
} {
|
|
name := name // make local to this closure
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
runTest(t, "testdata/"+name)
|
|
})
|
|
}
|
|
}
|
|
|
|
func runTest(t *testing.T, pathPrefix string) {
|
|
// Read the input IR.
|
|
ctx := llvm.NewContext()
|
|
defer ctx.Dispose()
|
|
buf, err := llvm.NewMemoryBufferFromFile(pathPrefix + ".ll")
|
|
os.Stat(pathPrefix + ".ll") // make sure this file is tracked by `go test` caching
|
|
if err != nil {
|
|
t.Fatalf("could not read file %s: %v", pathPrefix+".ll", err)
|
|
}
|
|
mod, err := ctx.ParseIR(buf)
|
|
if err != nil {
|
|
t.Fatalf("could not load module:\n%v", err)
|
|
}
|
|
defer mod.Dispose()
|
|
|
|
// Perform the transform.
|
|
err = Run(mod, 10*time.Minute, DefaultMaxInterpBlockEntries, false)
|
|
if err != nil {
|
|
if err, match := err.(*Error); match {
|
|
println(err.Error())
|
|
if len(err.Inst) != 0 {
|
|
println(err.Inst)
|
|
}
|
|
if len(err.Traceback) > 0 {
|
|
println("\ntraceback:")
|
|
for _, line := range err.Traceback {
|
|
println(line.Pos.String() + ":")
|
|
println(line.Inst)
|
|
}
|
|
}
|
|
}
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// To be sure, verify that the module is still valid.
|
|
if llvm.VerifyModule(mod, llvm.PrintMessageAction) != nil {
|
|
t.FailNow()
|
|
}
|
|
|
|
// Run some cleanup passes to get easy-to-read outputs.
|
|
to := llvm.NewPassBuilderOptions()
|
|
defer to.Dispose()
|
|
mod.RunPasses("globalopt,dse,adce", llvm.TargetMachine{}, to)
|
|
|
|
// Read the expected output IR.
|
|
out, err := os.ReadFile(pathPrefix + ".out.ll")
|
|
if err != nil {
|
|
t.Fatalf("could not read output file %s: %v", pathPrefix+".out.ll", err)
|
|
}
|
|
|
|
// See whether the transform output matches with the expected output IR.
|
|
expected := string(out)
|
|
actual := mod.String()
|
|
if !fuzzyEqualIR(expected, actual) {
|
|
t.Logf("output does not match expected output:\n%s", actual)
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
// fuzzyEqualIR returns true if the two LLVM IR strings passed in are roughly
|
|
// equal. That means, only relevant lines are compared (excluding comments
|
|
// etc.).
|
|
func fuzzyEqualIR(s1, s2 string) bool {
|
|
lines1 := filterIrrelevantIRLines(strings.Split(s1, "\n"))
|
|
lines2 := filterIrrelevantIRLines(strings.Split(s2, "\n"))
|
|
if len(lines1) != len(lines2) {
|
|
return false
|
|
}
|
|
for i, line1 := range lines1 {
|
|
line2 := lines2[i]
|
|
if line1 != line2 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// filterIrrelevantIRLines removes lines from the input slice of strings that
|
|
// are not relevant in comparing IR. For example, empty lines and comments are
|
|
// stripped out.
|
|
func filterIrrelevantIRLines(lines []string) []string {
|
|
var out []string
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line) // drop '\r' on Windows
|
|
if line == "" || line[0] == ';' {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "source_filename = ") {
|
|
continue
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
return out
|
|
}
|