Add visual indication of inlined frames. (#723)

* Add visual indication of inlined frames.

Mark frames sent to javascript with a boolean "Inlined" field. Use
this field to change the display by (1) adding an "(inlined)" marker
to the tooltip for the frame, and (2) by dropping any border between
the caller and the caller.

Document flame graph display, including coloring and visual indication
of inlining.

* Tweak doc changes based on review feedback.
diff --git a/doc/README.md b/doc/README.md
index 12c6dd0..d931007 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -434,12 +434,12 @@
 Flame Graph
 :   Displays a [flame graph](https://www.brendangregg.com/flamegraphs.html).
 
-Flame (experimental)
+[Flame Graph (new)](#flame-graph)
 :   Displays a view similar to a flame graph that can show the selected node's
     callers and callees simultaneously.
 
-NOTE: This view is currently experimental and may eventually replace the normal
-Flame Graph view.
+    NOTE: This view is currently experimental and may eventually replace the normal
+    Flame Graph view.
 
 Peek
 :   Shows callers / callees per function in a simple textual forma.
@@ -471,6 +471,34 @@
 right-hand side of such an entry deletes the configuration (after
 prompting the user to confirm).
 
+## Flame graph
+
+The `Flame graph (new)` view displays profile information as a
+[flame graph](https://www.brendangregg.com/flamegraphs.html).
+
+Boxes on this view correspond to stack frames in the profile. Caller boxes are
+directly above callee boxes. The width of each box is proportional to the sum of
+the sample value of profile samples where that frame was present on the call
+stack. Children of a particular box are laid out left to right in decreasing
+size order.
+
+Names displayed in different boxes may have different font sizes. These size
+differences are due to an attempt to fit as much of the name into the box as
+possible; no other interpretation should be placed on the size.
+
+Boxes are colored according to the name of the package in which the corresponding
+function occurs. E.g., in C++ profiles all frames corresponding to `std::` functions
+will be assigned the same color.
+
+In addition to the package-based coloring, the left hand side of a box may be
+darker. This darker area corresponds to the sum of the sample values of samples
+that occurred directly in the code representated by the box (as opposed to
+samples in functions called by the code.)
+
+Inlining is indicated by the absence of a horizontal border between a caller and
+a callee. E.g., suppose X calls Y calls Z and the call from Y to Z is inlined into
+Y. There will be a black border between X and Y, but no border between Y and Z.
+
 ## TODO: cover the following issues:
 
 *   Overall layout
diff --git a/internal/driver/html/stacks.css b/internal/driver/html/stacks.css
index d3701e4..d142aa7 100644
--- a/internal/driver/html/stacks.css
+++ b/internal/driver/html/stacks.css
@@ -27,6 +27,11 @@
   border-width: 0px;
   position: absolute;
   overflow: hidden;
+  box-sizing: border-box;
+}
+/* Not-inlined frames are visually separated from their caller. */
+.not-inlined {
+  border-top: 1px solid black;
 }
 /* Function name */
 .boxtext {
diff --git a/internal/driver/html/stacks.js b/internal/driver/html/stacks.js
index f0cb712..64229a0 100644
--- a/internal/driver/html/stacks.js
+++ b/internal/driver/html/stacks.js
@@ -176,8 +176,9 @@
 
   function handleEnter(box, div) {
     if (actionMenuOn) return;
+    const src = stacks.Sources[box.src];
     const d = details(box);
-    div.title = d + ' ' + stacks.Sources[box.src].FullName;
+    div.title = d + ' ' + src.FullName + (src.Inlined ? "\n(inlined)" : "");
     detailBox.innerText = d;
     // Highlight all boxes that have the same source as box.
     toggleClass(box.src, 'hilite2', true);
@@ -371,10 +372,13 @@
     r.style.left = box.x + 'px';
     r.style.top = box.y + 'px';
     r.style.width = w + 'px';
-    r.style.height = (ROW-1) + 'px';  // Leave 1px gap
+    r.style.height = ROW + 'px';
     r.classList.add('boxbg');
     r.style.background = makeColor(src.Color);
     addElem(srcIndex, r);
+    if (!src.Inlined) {
+      r.classList.add('not-inlined');
+    }
 
     // Box that shows time spent in self
     if (box.selfWidth >= MIN_WIDTH) {
diff --git a/internal/report/report_test.go b/internal/report/report_test.go
index b3e70b2..de1a2fd 100644
--- a/internal/report/report_test.go
+++ b/internal/report/report_test.go
@@ -233,6 +233,20 @@
 			},
 		},
 	},
+	{
+		ID:      6,
+		Mapping: testM[0],
+		Line: []profile.Line{
+			{
+				Function: testF[3],
+				Line:     7,
+			},
+			{
+				Function: testF[2],
+				Line:     6,
+			},
+		},
+	},
 }
 
 // testSample returns a profile sample with specified value and stack.
diff --git a/internal/report/stacks.go b/internal/report/stacks.go
index 4a5f5c1..7db51bc 100644
--- a/internal/report/stacks.go
+++ b/internal/report/stacks.go
@@ -48,6 +48,7 @@
 	FullName   string
 	FileName   string
 	UniqueName string // Disambiguates functions with same names
+	Inlined    bool   // If true this source was inlined into its caller
 
 	// Alternative names to display (with decreasing lengths) to make text fit.
 	// Guaranteed to be non-empty.
@@ -105,11 +106,16 @@
 }
 
 func (s *StackSet) makeInitialStacks(rpt *Report) {
-	srcs := map[profile.Line]int{} // Sources identified so far.
+	type key struct {
+		line    profile.Line
+		inlined bool
+	}
+	srcs := map[key]int{} // Sources identified so far.
 	seenFunctions := map[string]bool{}
 	unknownIndex := 1
-	getSrc := func(line profile.Line) int {
-		if i, ok := srcs[line]; ok {
+	getSrc := func(line profile.Line, inlined bool) int {
+		k := key{line, inlined}
+		if i, ok := srcs[k]; ok {
 			return i
 		}
 		x := StackSource{Places: []StackSlot{}} // Ensure Places is non-nil
@@ -128,10 +134,11 @@
 			x.UniqueName = x.FullName
 			unknownIndex++
 		}
+		x.Inlined = inlined
 		x.RE = "^" + regexp.QuoteMeta(x.UniqueName) + "$"
 		x.Display = shortNameList(x.FullName)
 		s.Sources = append(s.Sources, x)
-		srcs[line] = len(s.Sources) - 1
+		srcs[k] = len(s.Sources) - 1
 		return len(s.Sources) - 1
 	}
 
@@ -151,7 +158,8 @@
 			loc := sample.Location[i]
 			for j := len(loc.Line) - 1; j >= 0; j-- {
 				line := loc.Line[j]
-				stack.Sources = append(stack.Sources, getSrc(line))
+				inlined := (j != len(loc.Line)-1)
+				stack.Sources = append(stack.Sources, getSrc(line, inlined))
 			}
 		}
 
diff --git a/internal/report/stacks_test.go b/internal/report/stacks_test.go
index ecccd88..660b76d 100644
--- a/internal/report/stacks_test.go
+++ b/internal/report/stacks_test.go
@@ -78,16 +78,17 @@
 
 func TestStackSources(t *testing.T) {
 	// See report_test.go for the functions available to use in tests.
-	main, foo, bar, tee := testL[0], testL[1], testL[2], testL[3]
+	main, foo, bar, tee, inl := testL[0], testL[1], testL[2], testL[3], testL[5]
 
 	type srcInfo struct {
-		name string
-		self int64
+		name    string
+		self    int64
+		inlined bool
 	}
 
 	source := func(stacks StackSet, name string) srcInfo {
 		src := findSource(stacks, name)
-		return srcInfo{src.FullName, src.Self}
+		return srcInfo{src.FullName, src.Self, src.Inlined}
 	}
 
 	for _, c := range []struct {
@@ -108,10 +109,21 @@
 				testSample(1000, tee, main),
 			),
 			[]srcInfo{
-				{"main", 0},
-				{"bar", 100},
-				{"foo", 0},
-				{"tee", 1200},
+				{"main", 0, false},
+				{"bar", 100, false},
+				{"foo", 0, false},
+				{"tee", 1200, false},
+			},
+		},
+		{
+			"inlined",
+			makeTestStacks(
+				testSample(100, inl),
+				testSample(200, inl),
+			),
+			[]srcInfo{
+				// inl has bar->tee
+				{"tee", 300, true},
 			},
 		},
 		{
@@ -121,8 +133,8 @@
 				testSample(100, foo, foo, main),
 			),
 			[]srcInfo{
-				{"main", 0},
-				{"foo", 200},
+				{"main", 0, false},
+				{"foo", 200, false},
 			},
 		},
 		{
@@ -134,10 +146,10 @@
 				testSample(100, tee),
 			),
 			[]srcInfo{
-				{"main", 100},
-				{"bar", 100},
-				{"foo", 100},
-				{"tee", 100},
+				{"main", 100, false},
+				{"bar", 100, false},
+				{"foo", 100, false},
+				{"tee", 100, false},
 			},
 		},
 	} {