pkg/symbolizer: add Cache type

When the same crash happens all over again,
we repeatedly symbolize the same PCs.
This is slow and blocks VM loop in the manager.
Cache PCs we already symbolize, we are likely
to symbolize them again.
diff --git a/pkg/report/linux.go b/pkg/report/linux.go
index a1dafcc..43f55d9 100644
--- a/pkg/report/linux.go
+++ b/pkg/report/linux.go
@@ -34,6 +34,7 @@
 	reportStartIgnores    []*regexp.Regexp
 	infoMessagesWithStack [][]byte
 	eoi                   []byte
+	symbolizerCache       symbolizer.Cache
 }
 
 func ctorLinux(cfg *config) (reporterImpl, []string, error) {
@@ -399,13 +400,16 @@
 func (ctx *linux) symbolize(rep *Report) error {
 	symb := symbolizer.NewSymbolizer(ctx.config.target)
 	defer symb.Close()
+	symbFunc := func(bin string, pc uint64) ([]symbolizer.Frame, error) {
+		return ctx.symbolizerCache.Symbolize(symb.Symbolize, bin, pc)
+	}
 	var symbolized []byte
 	s := bufio.NewScanner(bytes.NewReader(rep.Report))
 	prefix := rep.reportPrefixLen
 	for s.Scan() {
 		line := append([]byte{}, s.Bytes()...)
 		line = append(line, '\n')
-		newLine := symbolizeLine(symb.Symbolize, ctx.symbols, ctx.vmlinux, ctx.kernelBuildSrc, line)
+		newLine := symbolizeLine(symbFunc, ctx.symbols, ctx.vmlinux, ctx.kernelBuildSrc, line)
 		if prefix > len(symbolized) {
 			prefix += len(newLine) - len(line)
 		}
diff --git a/pkg/symbolizer/cache.go b/pkg/symbolizer/cache.go
new file mode 100644
index 0000000..0a2fef3
--- /dev/null
+++ b/pkg/symbolizer/cache.go
@@ -0,0 +1,38 @@
+// Copyright 2024 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package symbolizer
+
+import (
+	"sync"
+)
+
+type Cache struct {
+	mu    sync.RWMutex
+	cache map[cacheKey]cacheVal
+}
+
+type cacheKey struct {
+	bin string
+	pc  uint64
+}
+
+type cacheVal struct {
+	frames []Frame
+	err    error
+}
+
+func (c *Cache) Symbolize(inner func(string, uint64) ([]Frame, error), bin string, pc uint64) ([]Frame, error) {
+	key := cacheKey{bin, pc}
+	c.mu.RLock()
+	val, ok := c.cache[key]
+	c.mu.RUnlock()
+	if ok {
+		return val.frames, val.err
+	}
+	frames, err := inner(bin, pc)
+	c.mu.Lock()
+	c.cache[key] = cacheVal{frames, err}
+	c.mu.Unlock()
+	return frames, err
+}