diff --git a/pkg/adhoc/server/convert.go b/pkg/adhoc/server/convert.go index 9f40e8b559..163ffa5a2f 100644 --- a/pkg/adhoc/server/convert.go +++ b/pkg/adhoc/server/convert.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strconv" "time" @@ -79,3 +80,47 @@ func collapsedToProfile(b []byte, name string, maxNodes int) (*flamebearer.Flame profile := flamebearer.NewProfile(out, maxNodes) return &profile, nil } + +func profileToTree(fb flamebearer.FlamebearerProfile) (*tree.Tree, error) { + if fb.Metadata.Format != string(tree.FormatSingle) { + return nil, fmt.Errorf("unsupported flamebearer format %s", fb.Metadata.Format) + } + return flamebearerV1ToTree(fb.Flamebearer) +} + +func flamebearerV1ToTree(fb flamebearer.FlamebearerV1) (*tree.Tree, error) { + t := tree.New() + deltaDecoding(fb.Levels, 0, 4) + for i, l := range fb.Levels { + for j := 0; j < len(l); j += 4 { + self := l[j+2] + if self > 0 { + t.InsertStackString(buildStack(fb, i, j), uint64(self)) + } + } + } + return t, nil +} + +func deltaDecoding(levels [][]int, start, step int) { + for _, l := range levels { + prev := 0 + for i := start; i < len(l); i += step { + delta := l[i] + l[i+1] + l[i] += prev + prev += delta + } + } +} + +func buildStack(fb flamebearer.FlamebearerV1, level, idx int) []string { + stack := make([]string, level+1) + stack[level] = fb.Names[fb.Levels[level][idx+3]] + x := fb.Levels[level][idx] + for i := level - 1; i >= 0; i-- { + j := sort.Search(len(fb.Levels[i])/4, func(j int) bool { return fb.Levels[i][j*4] > x }) - 1 + stack[i] = fb.Names[fb.Levels[i][j*4+3]] + x = fb.Levels[i][j*4] + } + return stack +} diff --git a/pkg/adhoc/server/server.go b/pkg/adhoc/server/server.go index 571a52bd4a..ab4fdd61cf 100644 --- a/pkg/adhoc/server/server.go +++ b/pkg/adhoc/server/server.go @@ -16,6 +16,7 @@ import ( "github.com/sirupsen/logrus" "github.com/pyroscope-io/pyroscope/pkg/adhoc/util" + "github.com/pyroscope-io/pyroscope/pkg/storage" "github.com/pyroscope-io/pyroscope/pkg/structs/flamebearer" ) @@ -50,7 +51,7 @@ func (s *server) AddRoutes(r *mux.Router) http.HandlerFunc { if s.enabled { r.HandleFunc("/v1/profiles", s.Profiles) r.HandleFunc("/v1/profile/{id:[0-9a-f]+}", s.Profile) - r.HandleFunc("/v1/diff/{id1[0-9a-f]+}/{id2[0-9a-f]+}", s.Diff) + r.HandleFunc("/v1/diff/{left:[0-9a-f]+}/{right:[0-9a-f]+}", s.Diff) } return r.ServeHTTP } @@ -126,8 +127,67 @@ func (s *server) Profile(w http.ResponseWriter, r *http.Request) { } } -// TODO(abeaumont) -func (*server) Diff(_ http.ResponseWriter, _ *http.Request) { +// Diff retrieves two different local files identified by their IDs and builds a profile diff. +func (s *server) Diff(w http.ResponseWriter, r *http.Request) { + lid := mux.Vars(r)["left"] + rid := mux.Vars(r)["right"] + s.mtx.RLock() + lp, lok := s.profiles[lid] + rp, rok := s.profiles[rid] + s.mtx.RUnlock() + if !lok { + s.log.WithField("id", lid).Warning("Profile does not exist") + http.Error(w, "Not Found", http.StatusNotFound) + return + } + if !rok { + s.log.WithField("id", rid).Warning("Profile does not exist") + http.Error(w, "Not Found", http.StatusNotFound) + return + } + lfb, err := s.convert(lp) + if err != nil { + s.log.WithError(err).Error("Unable to process left profile") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + rfb, err := s.convert(rp) + if err != nil { + s.log.WithError(err).Error("Unable to process right profile") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // TODO(abeaumont): Validate that profiles are comparable + // TODO(abeaumont): Simplify profile generation + out := &storage.GetOutput{ + Tree: nil, + Units: lfb.Metadata.Units, + SpyName: lfb.Metadata.SpyName, + SampleRate: lfb.Metadata.SampleRate, + } + lt, err := profileToTree(*lfb) + if err != nil { + s.log.WithError(err).Error("Unable to convert profile to tree") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + rt, err := profileToTree(*rfb) + if err != nil { + s.log.WithError(err).Error("Unable to convert profile to tree") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + lOut := &storage.GetOutput{Tree: lt} + rOut := &storage.GetOutput{Tree: rt} + + fb := flamebearer.NewCombinedProfile(out, lOut, rOut, s.maxNodes) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(fb); err != nil { + s.log.WithError(err).Error("Unable to encode the profile diff") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } } type converterFn func(b []byte, name string, maxNodes int) (*flamebearer.FlamebearerProfile, error) diff --git a/pkg/storage/tree/tree.go b/pkg/storage/tree/tree.go index a8851f5431..c1ec361d08 100644 --- a/pkg/storage/tree/tree.go +++ b/pkg/storage/tree/tree.go @@ -127,6 +127,39 @@ func (n *treeNode) insert(targetLabel []byte) *treeNode { return n.ChildrenNodes[i] } +func (n *treeNode) insertString(targetLabel string) *treeNode { + i, j := 0, len(n.ChildrenNodes) + for i < j { + m := (i + j) >> 1 + for k, b := range []byte(targetLabel) { + if k >= len(n.ChildrenNodes[m].Name) || b > n.ChildrenNodes[m].Name[k] { + // targetLabel > n.ChildrenNodes[m].Name + i = m + 1 + break + } + if b < n.ChildrenNodes[m].Name[k] { + // targetLabel < n.ChildrenNodes[m].Name + j = m + break + } + if k == len(targetLabel)-1 { + if len(targetLabel) == len(n.ChildrenNodes[m].Name) { + // targetLabel == n.ChildrenNodes[m].Name + return n.ChildrenNodes[m] + } + // targetLabel < n.ChildrenNodes[m].Name + j = m + } + } + } + l := []byte(targetLabel) + child := newNode(l) + n.ChildrenNodes = append(n.ChildrenNodes, child) + copy(n.ChildrenNodes[i+1:], n.ChildrenNodes[i:]) + n.ChildrenNodes[i] = child + return n.ChildrenNodes[i] +} + func (t *Tree) InsertInt(key []byte, value int) { t.Insert(key, uint64(value)) } func (t *Tree) Insert(key []byte, value uint64) { @@ -146,6 +179,17 @@ func (t *Tree) Insert(key []byte, value uint64) { node.Total += value } +func (t *Tree) InsertStackString(stack []string, v uint64) { + n := t.root + for j := range stack { + n.Total += v + n = n.insertString(stack[j]) + } + // Leaf. + n.Total += v + n.Self += v +} + func (t *Tree) Iterate(cb func(key []byte, val uint64)) { nodes := []*treeNode{t.root} prefixes := make([][]byte, 1) diff --git a/pkg/storage/tree/tree_test.go b/pkg/storage/tree/tree_test.go index 82fd7cb228..c55c4dbafc 100644 --- a/pkg/storage/tree/tree_test.go +++ b/pkg/storage/tree/tree_test.go @@ -36,6 +36,146 @@ var _ = Describe("tree package", func() { }) }) + Context("InsertStackString unsorted of length 1", func() { + tree := New() + tree.InsertStackString([]string{"a", "b"}, uint64(1)) + tree.InsertStackString([]string{"a", "a"}, uint64(2)) + + It("properly sets up a tree", func() { + Expect(tree.root.ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes).To(HaveLen(2)) + Expect(tree.root.ChildrenNodes[0].Self).To(Equal(uint64(0))) + Expect(tree.root.ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Self).To(Equal(uint64(2))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Self).To(Equal(uint64(1))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Total).To(Equal(uint64(2))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Total).To(Equal(uint64(1))) + Expect(tree.String()).To(Equal("a;a 2\na;b 1\n")) + }) + }) + + Context("InsertStackString equal of length 1", func() { + tree := New() + tree.InsertStackString([]string{"a", "b"}, uint64(1)) + tree.InsertStackString([]string{"a", "b"}, uint64(2)) + + It("properly sets up a tree", func() { + Expect(tree.root.ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].Self).To(Equal(uint64(0))) + Expect(tree.root.ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Self).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.String()).To(Equal("a;b 3\n")) + }) + }) + + Context("InsertStackString sorted of length 1", func() { + tree := New() + tree.InsertStackString([]string{"a", "b"}, uint64(1)) + tree.InsertStackString([]string{"a", "c"}, uint64(2)) + + It("properly sets up a tree", func() { + Expect(tree.root.ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes).To(HaveLen(2)) + Expect(tree.root.ChildrenNodes[0].Self).To(Equal(uint64(0))) + Expect(tree.root.ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Self).To(Equal(uint64(1))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Self).To(Equal(uint64(2))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Total).To(Equal(uint64(1))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Total).To(Equal(uint64(2))) + Expect(tree.String()).To(Equal("a;b 1\na;c 2\n")) + }) + }) + + Context("InsertStackString sorted of different lengths", func() { + tree := New() + tree.InsertStackString([]string{"a", "b"}, uint64(1)) + tree.InsertStackString([]string{"a", "ba"}, uint64(2)) + + It("properly sets up a tree", func() { + Expect(tree.root.ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes).To(HaveLen(2)) + Expect(tree.root.ChildrenNodes[0].Self).To(Equal(uint64(0))) + Expect(tree.root.ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Self).To(Equal(uint64(1))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Self).To(Equal(uint64(2))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Total).To(Equal(uint64(1))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Total).To(Equal(uint64(2))) + Expect(tree.String()).To(Equal("a;b 1\na;ba 2\n")) + }) + }) + + Context("InsertStackString unsorted of different lengths", func() { + tree := New() + tree.InsertStackString([]string{"a", "ba"}, uint64(1)) + tree.InsertStackString([]string{"a", "b"}, uint64(2)) + + It("properly sets up a tree", func() { + Expect(tree.root.ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes).To(HaveLen(2)) + Expect(tree.root.ChildrenNodes[0].Self).To(Equal(uint64(0))) + Expect(tree.root.ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Self).To(Equal(uint64(2))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Self).To(Equal(uint64(1))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Total).To(Equal(uint64(2))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Total).To(Equal(uint64(1))) + Expect(tree.String()).To(Equal("a;b 2\na;ba 1\n")) + }) + }) + + Context("InsertStackString unsorted of length 2", func() { + tree := New() + tree.InsertStackString([]string{"a", "bb"}, uint64(1)) + tree.InsertStackString([]string{"a", "ba"}, uint64(2)) + + It("properly sets up a tree", func() { + Expect(tree.root.ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes).To(HaveLen(2)) + Expect(tree.root.ChildrenNodes[0].Self).To(Equal(uint64(0))) + Expect(tree.root.ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Self).To(Equal(uint64(2))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Self).To(Equal(uint64(1))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Total).To(Equal(uint64(2))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Total).To(Equal(uint64(1))) + Expect(tree.String()).To(Equal("a;ba 2\na;bb 1\n")) + }) + }) + + Context("InsertStackString equal of length 2", func() { + tree := New() + tree.InsertStackString([]string{"a", "bb"}, uint64(1)) + tree.InsertStackString([]string{"a", "bb"}, uint64(2)) + + It("properly sets up a tree", func() { + Expect(tree.root.ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].Self).To(Equal(uint64(0))) + Expect(tree.root.ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Self).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.String()).To(Equal("a;bb 3\n")) + }) + }) + + Context("InsertStackString sorted of length 2", func() { + tree := New() + tree.InsertStackString([]string{"a", "bb"}, uint64(1)) + tree.InsertStackString([]string{"a", "bc"}, uint64(2)) + + It("properly sets up a tree", func() { + Expect(tree.root.ChildrenNodes).To(HaveLen(1)) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes).To(HaveLen(2)) + Expect(tree.root.ChildrenNodes[0].Self).To(Equal(uint64(0))) + Expect(tree.root.ChildrenNodes[0].Total).To(Equal(uint64(3))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Self).To(Equal(uint64(1))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Self).To(Equal(uint64(2))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[0].Total).To(Equal(uint64(1))) + Expect(tree.root.ChildrenNodes[0].ChildrenNodes[1].Total).To(Equal(uint64(2))) + Expect(tree.String()).To(Equal("a;bb 1\na;bc 2\n")) + }) + }) + Context("Merge", func() { Context("similar trees", func() { treeA := New() diff --git a/webapp/javascript/components/AdhocComparison.tsx b/webapp/javascript/components/AdhocComparison.tsx index 1243cc3139..d603152a12 100644 --- a/webapp/javascript/components/AdhocComparison.tsx +++ b/webapp/javascript/components/AdhocComparison.tsx @@ -152,11 +152,11 @@ const mapStateToProps = (state) => ({ ...state.root, leftFile: state.root.adhocComparison.left.file, leftFlamebearer: state.root.adhocComparison.left.flamebearer, - leftProfile: state.root.adhocComparison.left.profile, + leftProfile: state.root.adhocShared.left.profile, isLeftProfileLoading: state.root.adhocComparison.left.isProfileLoading, rightFile: state.root.adhocComparison.right.file, rightFlamebearer: state.root.adhocComparison.right.flamebearer, - rightProfile: state.root.adhocComparison.right.profile, + rightProfile: state.root.adhocShared.right.profile, isRightProfileLoading: state.root.adhocComparison.right.isProfileLoading, }); diff --git a/webapp/javascript/components/AdhocComparisonDiff.tsx b/webapp/javascript/components/AdhocComparisonDiff.tsx new file mode 100644 index 0000000000..dc7c215bf7 --- /dev/null +++ b/webapp/javascript/components/AdhocComparisonDiff.tsx @@ -0,0 +1,123 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import 'react-dom'; + +import { bindActionCreators } from 'redux'; +import Box from '@ui/Box'; +import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; +import Spinner from 'react-svg-spinner'; +import classNames from 'classnames'; +import FileList from './FileList'; +import FlameGraphRenderer from './FlameGraph'; +import Footer from './Footer'; +import { + fetchAdhocProfiles, + fetchAdhocProfileDiff, + setAdhocLeftProfile, + setAdhocRightProfile, +} from '../redux/actions'; +import styles from './ComparisonApp.module.css'; +import 'react-tabs/style/react-tabs.css'; +import adhocStyles from './Adhoc.module.scss'; + +function AdhocComparisonDiff(props) { + const { actions, isProfileLoading, flamebearer, leftProfile, rightProfile } = + props; + const { setAdhocLeftProfile, setAdhocRightProfile } = actions; + + useEffect(() => { + actions.fetchAdhocProfiles(); + return actions.abortFetchAdhocProfiles; + }, []); + + useEffect(() => { + if (leftProfile && rightProfile) { + actions.fetchAdhocProfileDiff(leftProfile, rightProfile); + } + return actions.abortFetchAdhocProfileDiff; + }, [leftProfile, rightProfile]); + + return ( +