cmd/viewcore: handle 1.14 free spans

Free spans are now implicit. Derive the free span information
by finding all the memory in the inuse address ranges that isn't
attributed to any span.

Fixes viewcore for all versions of 1.14.
Also works for tip.

Fixes golang/go#38638

Change-Id: Ied283f59391aad8ed5b1b657d2fd6e89e9e9ae6c
Reviewed-on: https://go-review.googlesource.com/c/debug/+/232161
Run-TryBot: Keith Randall <khr@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
diff --git a/internal/gocore/gocore_test.go b/internal/gocore/gocore_test.go
index bdb843d..51c5bee 100644
--- a/internal/gocore/gocore_test.go
+++ b/internal/gocore/gocore_test.go
@@ -270,4 +270,5 @@
 	loadExampleVersion(t, "1.12.zip")
 	loadExampleVersion(t, "1.13.zip")
 	loadExampleVersion(t, "1.13.3.zip")
+	loadExampleVersion(t, "1.14.zip")
 }
diff --git a/internal/gocore/process.go b/internal/gocore/process.go
index ca83b94..2746578 100644
--- a/internal/gocore/process.go
+++ b/internal/gocore/process.go
@@ -7,6 +7,7 @@
 import (
 	"debug/dwarf"
 	"fmt"
+	"math/bits"
 	"strings"
 	"sync"
 
@@ -306,7 +307,7 @@
 			panic("weird mapping " + m.Perm().String())
 		}
 	}
-	if mheap.HasField("curArena") {
+	if mheap.HasField("curArena") { // go1.13.3 and up
 		// Subtract from the heap unallocated space
 		// in the current arena.
 		ca := mheap.Field("curArena")
@@ -431,6 +432,54 @@
 			}
 		}
 	}
+	if mheap.HasField("pages") { // go1.14+
+		// There are no longer "free" mspans to represent unused pages.
+		// Instead, there are just holes in the pagemap into which we can allocate.
+		// Look through the page allocator and count the total free space.
+		// Also keep track of how much has been scavenged.
+		pages := mheap.Field("pages")
+		chunks := pages.Field("chunks")
+		arenaBaseOffset := p.rtConstants["arenaBaseOffset"]
+		pallocChunkBytes := p.rtConstants["pallocChunkBytes"]
+		pallocChunksL1Bits := p.rtConstants["pallocChunksL1Bits"]
+		pallocChunksL2Bits := p.rtConstants["pallocChunksL2Bits"]
+		inuse := pages.Field("inUse")
+		ranges := inuse.Field("ranges")
+		for i := int64(0); i < ranges.SliceLen(); i++ {
+			r := ranges.SliceIndex(i)
+			base := core.Address(r.Field("base").Uintptr())
+			limit := core.Address(r.Field("limit").Uintptr())
+			chunkBase := (int64(base) + arenaBaseOffset) / pallocChunkBytes
+			chunkLimit := (int64(limit) + arenaBaseOffset) / pallocChunkBytes
+			for chunkIdx := chunkBase; chunkIdx < chunkLimit; chunkIdx++ {
+				var l1, l2 int64
+				if pallocChunksL1Bits == 0 {
+					l2 = chunkIdx
+				} else {
+					l1 = chunkIdx >> uint(pallocChunksL2Bits)
+					l2 = chunkIdx & (1<<uint(pallocChunksL2Bits) - 1)
+				}
+				chunk := chunks.ArrayIndex(l1).Deref().ArrayIndex(l2)
+				// Count the free bits in this chunk.
+				alloc := chunk.Field("pallocBits")
+				for i := int64(0); i < pallocChunkBytes/pageSize/64; i++ {
+					freeSpanSize += int64(bits.OnesCount64(^alloc.ArrayIndex(i).Uint64())) * pageSize
+				}
+				// Count the scavenged bits in this chunk.
+				scavenged := chunk.Field("scavenged")
+				for i := int64(0); i < pallocChunkBytes/pageSize/64; i++ {
+					releasedSpanSize += int64(bits.OnesCount64(scavenged.ArrayIndex(i).Uint64())) * pageSize
+				}
+			}
+		}
+		// Also count pages in the page cache for each P.
+		allp := p.rtGlobals["allp"]
+		for i := int64(0); i < allp.SliceLen(); i++ {
+			pcache := allp.SliceIndex(i).Deref().Field("pcache")
+			freeSpanSize += int64(bits.OnesCount64(pcache.Field("cache").Uint64())) * pageSize
+			releasedSpanSize += int64(bits.OnesCount64(pcache.Field("scav").Uint64())) * pageSize
+		}
+	}
 
 	p.stats = &Stats{"all", all, []*Stats{
 		&Stats{"text", text, nil},
diff --git a/internal/gocore/testdata/1.14.zip b/internal/gocore/testdata/1.14.zip
new file mode 100644
index 0000000..5f225ce
--- /dev/null
+++ b/internal/gocore/testdata/1.14.zip
Binary files differ