Skip to content

Commit

Permalink
Enhancement/split flamegraphrenderer (#360)
Browse files Browse the repository at this point in the history
* split flamegraph rendere into smaller components

* use flamegraph renderer from external repo

* use flamegraph as local component

* disable lint some checks

* fix paramsToObject function and tooltip position

* add cypress test

* fix textformat and color generation

* adds a webpack config for grafana panel (#382)

* improves license noticies

* changes tooltip position to fixed

* fixes a key in array bug

* improves tooltip positioning

Co-authored-by: Dmitry Filimonov <dmitry@pyroscope.io>
  • Loading branch information
Loggy and petethepig authored Sep 9, 2021
1 parent 2539b9c commit 230699f
Show file tree
Hide file tree
Showing 31 changed files with 1,529 additions and 879 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ build: ## Builds the binary
build-release: embedded-assets ## Builds the release build
EXTRA_GO_TAGS=,embedassets $(MAKE) build

.PHONY: build-panel
build-panel:
NODE_ENV=production $(shell yarn bin webpack) --config scripts/webpack/webpack.panel.js

.PHONY: build-rust-dependencies
build-rust-dependencies:
ifeq ("$(OS)", "Linux")
Expand Down
9 changes: 8 additions & 1 deletion cypress/integration/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ describe('basic test', () => {
cy.location('pathname').should('eq', '/');
});

it('updates flamegraph on app name change', () => {
cy.visit('/')

cy.findByTestId('app-name-selector').select('pyroscope.server.cpu');
cy.findByTestId('flamegraph-canvas').invoke('attr', 'data-appname').should('eq', 'pyroscope.server.cpu{}');
});

it('view buttons should change view when clicked', () => {
cy.visit('/')
cy.findByTestId('btn-table-view').click();
cy.findByTestId('table-view').should('be.visible');
cy.findByTestId('flamegraph-view').should('not.be.visible');
cy.findByTestId('flamegraph-view').should('not.exist');

cy.findByTestId('btn-both-view').click();
cy.findByTestId('table-view').should('be.visible');
Expand Down
2 changes: 1 addition & 1 deletion cypress/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
};
2 changes: 1 addition & 1 deletion cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import '@testing-library/cypress/add-commands';
import "@testing-library/cypress/add-commands";
2 changes: 1 addition & 1 deletion cypress/support/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'
import "./commands";

// Alternatively you can use CommonJS syntax:
// require('./commands')
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"private": true,
"name": "pyroscope",
"version": "0.0.39",
"main": "webapp/javascript/components/FlameGraph/FlameGraphComponent/index.jsx",
"repository": {
"type": "git",
"url": "https://github.com/pyroscope-io/pyroscope.git"
Expand Down Expand Up @@ -99,7 +100,8 @@
"redux-promise": "^0.6.0",
"redux-query-sync": "^0.1.10",
"redux-thunk": "^2.3.0",
"sanitize.css": "^11.0.1"
"sanitize.css": "^11.0.1",
"style-loader": "^3.2.1"
},
"engines": {
"node": ">=11"
Expand Down
1 change: 0 additions & 1 deletion scripts/webpack/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ module.exports = {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
Expand Down
6 changes: 2 additions & 4 deletions scripts/webpack/webpack.dev.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
const { merge } = require("webpack-merge");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");

const common = require("./webpack.common.js");

module.exports = merge(common, {
mode: "development",
plugins: [
new BundleAnalyzerPlugin()
]
plugins: [new BundleAnalyzerPlugin()],
});
21 changes: 21 additions & 0 deletions scripts/webpack/webpack.panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { merge } = require("webpack-merge");

const prod = require("./webpack.prod.js");
const path = require("path");

module.exports = merge(prod, {
entry: {
flamegraphComponent: "./webapp/javascript/components/FlameGraph/FlameGraphComponent/index.jsx",
},

output: {
publicPath: "",
path: path.resolve(__dirname, "../../webapp/public/assets"),
filename: "[name].js",
clean: true,

library: "pyroscope",
libraryTarget: 'umd',
umdNamedDefine: true
},
});
1 change: 1 addition & 0 deletions webapp/__tests__/__snapshots__/NameSelector.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports[`NameSelector render correctly NameSelector component 1`] = `
Application: 
<select
className="label-select"
data-testid="app-name-selector"
onChange={[Function]}
value="hotrod.golang.customer"
>
Expand Down
2 changes: 1 addition & 1 deletion webapp/javascript/components/ComparisonApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { connect } from "react-redux";
import "react-dom";

import { bindActionCreators } from "redux";
import FlameGraphRenderer from "./FlameGraphRenderer";
import FlameGraphRenderer from "./FlameGraph";
import TimelineChartWrapper from "./TimelineChartWrapper";
import Header from "./Header";
import Footer from "./Footer";
Expand Down
2 changes: 1 addition & 1 deletion webapp/javascript/components/ComparisonDiffApp.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useRef } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import FlameGraphRenderer from "./FlameGraphRenderer";
import FlameGraphRenderer from "./FlameGraph";
import Header from "./Header";
import Footer from "./Footer";
import TimelineChartWrapper from "./TimelineChartWrapper";
Expand Down
3 changes: 1 addition & 2 deletions webapp/javascript/components/ExportData.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ function ExportData(props) {
};

const formatPdfTitle = () => {
const { value } = props.labels.filter((x) => x.name === "__name__")[0];
const { from, until } = props;

return `${value} - from: ${from} - to ${until}`;
return `${props.label} - from: ${from} - to ${until}`;
};

// export flamegraph canvas element
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable camelcase */
import Color from "color";
import murmurhash3_32_gc from "./murmur3";

const colors = [
Color.hsl(24, 69, 60),
Color.hsl(34, 65, 65),
Color.hsl(194, 52, 61),
Color.hsl(163, 45, 55),
Color.hsl(211, 48, 60),
Color.hsl(246, 40, 65),
Color.hsl(305, 63, 79),
Color.hsl(47, 100, 73),

Color.rgb(183, 219, 171),
Color.rgb(244, 213, 152),
Color.rgb(112, 219, 237),
Color.rgb(249, 186, 143),
Color.rgb(242, 145, 145),
Color.rgb(130, 181, 216),
Color.rgb(229, 168, 226),
Color.rgb(174, 162, 224),
Color.rgb(154, 196, 138),
Color.rgb(242, 201, 109),
Color.rgb(101, 197, 219),
Color.rgb(249, 147, 78),
Color.rgb(234, 100, 96),
Color.rgb(81, 149, 206),
Color.rgb(214, 131, 206),
Color.rgb(128, 110, 183),
];

export const defaultColor = Color.rgb(148, 142, 142);
export const diffColorRed = Color.rgb(200, 0, 0);
export const diffColorGreen = Color.rgb(0, 170, 0);

export function colorBasedOnPackageName(name, a) {
const hash = murmurhash3_32_gc(name);
const colorIndex = hash % colors.length;
const baseClr = colors[colorIndex];
return baseClr.alpha(a);
}

// assume: left >= 0 && Math.abs(diff) <= left so diff / left is in [0...1]
// if left == 0 || Math.abs(diff) > left, we use the color of 100%
export function colorBasedOnDiff(diff, left, a) {
const v =
!left || Math.abs(diff) > left ? 1 : 200 * Math.sqrt(Math.abs(diff / left));
if (diff >= 0) return Color.rgb(200, 200 - v, 200 - v).alpha(a);
return Color.rgb(200 - v, 200, 200 - v).alpha(a);
}

export function colorGreyscale(v, a) {
return Color.rgb(v, v, v).alpha(a);
}
170 changes: 170 additions & 0 deletions webapp/javascript/components/FlameGraph/FlameGraphComponent/format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/* eslint-disable no-plusplus */
/* eslint-disable prefer-destructuring */
/* eslint-disable max-classes-per-file */
export function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

const suffixes = ["K", "M", "G", "T"];

export function shortNumber(x) {
let suffix = "";

for (let i = 0; x > 1000 && i < suffixes.length; i++) {
suffix = suffixes[i];
x /= 1000;
}

return Math.round(x).toString() + suffix;
}

export function formatPercent(ratio) {
const percent = Math.round(10000 * ratio) / 100;
return `${percent}%`;
}

const durations = [
[60, "minute"],
[60, "hour"],
[24, "day"],
[30, "month"],
[12, "year"],
];

// this is a class and not a function because we can save some time by
// precalculating divider and suffix and not doing it on each iteration
export class DurationFormatter {
constructor(maxDur) {
this.divider = 1;
this.suffix = "second";
for (let i = 0; i < durations.length; i++) {
if (maxDur >= durations[i][0]) {
this.divider *= durations[i][0];
maxDur /= durations[i][0];
this.suffix = durations[i][1];
} else {
break;
}
}
}

format(samples, sampleRate) {
let number = samples / sampleRate / this.divider;
if (number >= 0 && number < 0.01) {
number = "< 0.01";
} else if (number <= 0 && number > -0.01) {
number = "< 0.01";
} else {
number = number.toFixed(2);
}
return `${number} ${this.suffix}${number === 1 ? "" : "s"}`;
}
}

const bytes = [
[1024, "KB"],
[1024, "MB"],
[1024, "GB"],
[1024, "TB"],
[1024, "PB"],
];

export class BytesFormatter {
constructor(maxBytes) {
this.divider = 1;
this.suffix = "bytes";
for (let i = 0; i < bytes.length; i++) {
if (maxBytes >= bytes[i][0]) {
this.divider *= bytes[i][0];
maxBytes /= bytes[i][0];
this.suffix = bytes[i][1];
} else {
break;
}
}
}

format(samples, sampleRate) {
let number = samples / this.divider;
if (number >= 0 && number < 0.01) {
number = "< 0.01";
} else if (number <= 0 && number > -0.01) {
number = "< 0.01";
} else {
number = number.toFixed(2);
}
return `${number} ${this.suffix}`;
}
}

const objects = [
[1000, "K"],
[1000, "M"],
[1000, "G"],
[1000, "T"],
[1000, "P"],
];

export class ObjectsFormatter {
constructor(maxObjects) {
this.divider = 1;
this.suffix = "";
for (let i = 0; i < objects.length; i++) {
if (maxObjects >= objects[i][0]) {
this.divider *= objects[i][0];
maxObjects /= objects[i][0];
this.suffix = objects[i][1];
} else {
break;
}
}
}

format(samples, sampleRate) {
let number = samples / this.divider;
if (number >= 0 && number < 0.01) {
number = "< 0.01";
} else if (number <= 0 && number > -0.01) {
number = "< 0.01";
} else {
number = number.toFixed(2);
}
return `${number} ${this.suffix}`;
}
}

export function getPackageNameFromStackTrace(spyName, stackTrace) {
// TODO: actually make sure these make sense and add tests
const regexpLookup = {
default: /^(?<packageName>(.*\/)*)(?<filename>.*)(?<line_info>.*)$/,
dotnetspy: /^(?<packageName>.+)\.(.+)\.(.+)\(.*\)$/,
ebpfspy: /^(?<packageName>.+)$/,
gospy: /^(?<packageName>(.*\/)*)(?<filename>.*)(?<line_info>.*)$/,
phpspy: /^(?<packageName>(.*\/)*)(?<filename>.*\.php+)(?<line_info>.*)$/,
pyspy: /^(?<packageName>(.*\/)*)(?<filename>.*\.py+)(?<line_info>.*)$/,
rbspy: /^(?<packageName>(.*\/)*)(?<filename>.*\.rb+)(?<line_info>.*)$/,
};

if (stackTrace.length === 0) {
return stackTrace;
}
const regexp = regexpLookup[spyName] || regexpLookup.default;
const fullStackGroups = stackTrace.match(regexp);
if (fullStackGroups) {
return fullStackGroups.groups.packageName;
}
return stackTrace;
}

export function getFormatter(max, sampleRate, units) {
switch (units) {
case "samples":
return new DurationFormatter(max / sampleRate);
case "objects":
return new ObjectsFormatter(max);
case "bytes":
return new BytesFormatter(max);
default:
return new DurationFormatter(max / sampleRate);
}
}
Loading

0 comments on commit 230699f

Please sign in to comment.