├── .gitignore ├── frontend ├── .browserslistrc ├── .flowconfig ├── .eslintrc.json ├── .babelrc ├── profiler │ ├── components │ │ ├── routine.fixture.js │ │ ├── CallersList.js │ │ ├── FlameGraph.js │ │ ├── RoutineOverviewPage.jsx │ │ ├── AllocationParts.jsx │ │ ├── AllocationTypeList.jsx │ │ ├── RoutineList.jsx │ │ ├── SpeshOverview.jsx │ │ ├── RoutinePieces.jsx │ │ ├── Routine.jsx │ │ ├── RoutinePaths.jsx │ │ ├── OverviewPage.js │ │ ├── AllocationViewer.jsx │ │ └── GCOverview.jsx │ ├── reducer.js │ └── actions.js ├── webpack.config.js ├── welcome │ ├── reducer.js │ ├── actions.js │ ├── CodeEditor.js │ └── GreetingsPage.js ├── explanations.js ├── package.json ├── heapanalyzer │ ├── actions.js │ ├── components │ │ ├── SnapshotList.jsx │ │ └── Graphs.jsx │ └── reducer.js └── index.js ├── appimage ├── stolen_icon.png ├── MoarPerf.desktop ├── moarperf-appimage-launcher ├── raku_skip_home_compunit_repository.patch ├── do_stuff.sh └── MoarPerf.yml ├── .idea ├── vcs.xml ├── misc.xml ├── modules.xml └── moarperf.iml ├── .cro.yml ├── MoarPerf.iml ├── lib ├── CodeRunner.pm6 ├── HeapAnalyzerWeb.pm6 └── Routes.pm6 ├── META6.json ├── AppMoarVMHeapAnalyzer.iml ├── static └── index.html ├── .travis.yml ├── service.p6 └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .precomp/ 2 | node_modules/ 3 | *.swp 4 | -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.25% 2 | not dead 3 | -------------------------------------------------------------------------------- /appimage/stolen_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timo/moarperf/HEAD/appimage/stolen_icon.png -------------------------------------------------------------------------------- /frontend/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /appimage/MoarPerf.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=MoarPerf 3 | Comment=Front-end for MoarVM's / Rakudo's profilers 4 | Exec=moarperf-appimage-launcher 5 | Icon=moarperf_stolen_icon 6 | Terminal=true 7 | Type=Application 8 | StartupNotify=false 9 | Categories=Development; 10 | -------------------------------------------------------------------------------- /appimage/moarperf-appimage-launcher: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | HERE="$(dirname "$(readlink -f "${0}")")" 3 | cd $HERE/../rakuapp 4 | env RAKUDO_HOME="$HERE/../rakudo/share/perl6" NQP_HOME="$HERE/../rakudo/share/nqp" LD_LIBRARY_PATH="$HERE/../rakudo/lib" "$HERE/../rakudo/bin/raku" service.p6 "$@" 5 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "react/jsx-filename-extension": "off", 6 | "no-nested-ternary": "off", 7 | "no-prototype-builtins": "off", 8 | "no-unused-expressions": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.cro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: moarperf 3 | cro: 1 4 | id: moarperf 5 | env: [] 6 | links: [] 7 | entrypoint: service.p6 8 | endpoints: 9 | - 10 | port-env: MOARPERF_PORT 11 | name: HTTP 12 | protocol: http 13 | host-env: MOARPERF_HOST 14 | id: http 15 | ignore: 16 | - .git 17 | - frontend 18 | - static 19 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets" : ["@babel/preset-flow", "@babel/preset-env", "@babel/preset-react"], 3 | "plugins" : ["@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-function-bind", "@babel/plugin-proposal-object-rest-spread", "@babel/plugin-transform-modules-commonjs"] 4 | } 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /MoarPerf.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/moarperf.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/profiler/components/routine.fixture.js: -------------------------------------------------------------------------------- 1 | import RoutineList from './RoutineList'; 2 | 3 | export default { 4 | component: RoutineList, 5 | props: { 6 | globalStats: { 7 | totalExclusiveTime: 1010, 8 | }, 9 | routines: [{ 10 | name: 'reify', 11 | filename: 'blah.p6', 12 | line: 1234, 13 | entries: 99999, 14 | exclusiveTime: 1000, 15 | }, 16 | { 17 | name: 'iterator', 18 | filename: 'SETTING::Bloop.p6', 19 | line: 99, 20 | entries: 2, 21 | exclusiveTime: 10, 22 | }], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/CodeRunner.pm6: -------------------------------------------------------------------------------- 1 | 2 | use OO::Monitors; 3 | 4 | unit monitor CodeRunner; 5 | 6 | method get-interesting-local-files(Str :$type) { 7 | given $type { 8 | when "script" { 9 | dir(test => /\.[p6|pm|pm6]/)>>.basename 10 | } 11 | when "profiles" { 12 | dir(test => /[^"heap-snapshot-" | '.sql'$ | '.sqlite3'$]/)>>.basename; 13 | } 14 | } 15 | } 16 | 17 | # One to write code to a temp file 18 | multi method run-program-with-profiler(Str :$code!) { 19 | 20 | } 21 | 22 | multi method run-program-with-profiler(Str :$filename!) { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: [ 5 | './index.js' 6 | ], 7 | context: path.resolve(__dirname), 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.jsx?$/, 12 | exclude: /node_modules/, 13 | resolve: { extensions: [".js", ".jsx"] }, 14 | use: { 15 | loader: "babel-loader" 16 | } 17 | } 18 | ] 19 | }, 20 | devtool: "eval-source-map", 21 | output: { 22 | path: path.resolve(__dirname, '../static/js/'), 23 | publicPath: "/js/" 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /META6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MoarPerf", 3 | "description": "Web App to analyze perl6 program performance", 4 | "version": "0.1", 5 | "perl": "6.*", 6 | "authors": [ 7 | "Timo Paulssen" 8 | ], 9 | "depends": [ 10 | "JSON::Fast", 11 | "OO::Monitors", 12 | "Cro::HTTP", 13 | "Cro::WebSocket", 14 | "App::MoarVM::HeapAnalyzer", 15 | "Digest::SHA1::Native", 16 | "DBIish" 17 | ], 18 | "provides": { 19 | "CodeRunner": "lib/CodeRunner.pm6", 20 | "HeapAnalyzerWeb": "lib/HeapAnalyzerWeb.pm6", 21 | "ProfilerWeb": "lib/ProfilerWeb.pm6", 22 | "Routes": "lib/Routes.pm6" 23 | }, 24 | "resources": [], 25 | "license": "Artistic-2.0", 26 | "source-url": "https://github.com/timo/moarperf" 27 | } -------------------------------------------------------------------------------- /AppMoarVMHeapAnalyzer.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /appimage/raku_skip_home_compunit_repository.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/core.c/CompUnit/RepositoryRegistry.pm6 b/src/core.c/CompUnit/RepositoryRegistry.pm6 2 | index 165c5918e..a6da409fc 100644 3 | --- a/src/core.c/CompUnit/RepositoryRegistry.pm6 4 | +++ b/src/core.c/CompUnit/RepositoryRegistry.pm6 5 | @@ -140,6 +140,7 @@ class CompUnit::RepositoryRegistry { 6 | my str $home; 7 | my str $home-spec; 8 | 9 | + #`« 10 | if nqp::ifnull( 11 | nqp::atkey($ENV,'HOME'), 12 | nqp::concat( 13 | @@ -150,6 +151,7 @@ class CompUnit::RepositoryRegistry { 14 | $home = $home-path ~ $sep ~ '.raku'; 15 | $home-spec = 'inst#' ~ $home; 16 | } 17 | + » 18 | 19 | unless $precomp-specs { 20 | nqp::bindkey($custom-lib,'core', 21 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | MoarVM Performance Tool 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/welcome/reducer.js: -------------------------------------------------------------------------------- 1 | import type { WelcomeAction } from './actions'; 2 | import * as ActionTypes from './actions'; 3 | 4 | export type WelcomeState = { 5 | +filePath: string, 6 | +frontendMode: string, 7 | } 8 | 9 | const initialState : WelcomeState = { 10 | filePath: '', 11 | frontendMode: "welcome", 12 | }; 13 | export default function welcomeReducer( 14 | state : WelcomeState = initialState, 15 | action : WelcomeAction, 16 | ) : WelcomeState { 17 | switch (action.type) { 18 | case ActionTypes.CHANGE_FILE_PATH: { 19 | return { ...state, filePath: action.path }; 20 | } 21 | case ActionTypes.FILE_REQUESTED: { 22 | return { 23 | ...state, 24 | frontendMode: action.filetype === 'profile' 25 | ? 'profile' 26 | : 'heapsnapshot' 27 | }; 28 | } 29 | default: 30 | return { ...state }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/explanations.js: -------------------------------------------------------------------------------- 1 | 2 | export const explanations = { 3 | objectHasExtraData: "these objects hold on to some extra data that can differ in size from object to object, like an array or hash.", 4 | reprs: { 5 | NativeRef: "Temporary objects used to make native lexicals, locals, attributes, and positional values usable like a read-write Scalar.", 6 | VMHash: "Representation for objects that have key-value access using a hash table.", 7 | MVMContext: "Objects used to hold on to lexical scopes for later traversal.", 8 | ConcBlockingQueue: "Objects that implement a queue that blocks when no value is present while trying to receive one.", 9 | }, 10 | types: { 11 | BOOTCode: `This is the type for closures. Whenever a routine or block gets returned from a function, it holds on to data from that function and outer scopes. These are represented in a BOOTCode object.`, 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/welcome/actions.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | export const CHANGE_FILE_PATH = 'CHANGE_FILE_PATH'; 4 | export const FILE_REQUESTED = 'FILE_REQUESTED'; 5 | 6 | type ChangeFilePathAction = { type : "CHANGE_FILE_PATH", path: string }; 7 | type RequestFileAction = { type : "REQUEST_FILE", filetype: string }; 8 | 9 | export type WelcomeAction = ChangeFilePathAction | 10 | RequestFileAction; 11 | 12 | export function changeFilePath(path : string) : ChangeFilePathAction { 13 | return { type: CHANGE_FILE_PATH, path }; 14 | } 15 | 16 | export function requestFile() : RequestFileAction { 17 | return (dispatch, getState) => { 18 | $.ajax({ 19 | url: '/load-file', 20 | type: 'POST', 21 | contentType: 'application/json', 22 | data: JSON.stringify({ path: getState().welcome.filePath }), 23 | success: ({ filetype }) => dispatch({ type: FILE_REQUESTED, filetype }), 24 | }); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: xenial 3 | branches: 4 | except: 5 | - /^untagged.*/ 6 | node_js: 7 | - stable 8 | install: 9 | - "tar --help" 10 | - "cd frontend" 11 | - "npm install" 12 | - "npm run build" 13 | # we don't actually do tests here 14 | script: 15 | - "cd $TRAVIS_BUILD_DIR" 16 | - "sh ./appimage/do_stuff.sh" 17 | - "true" 18 | - "pwd" 19 | - "ls -l" 20 | 21 | before_deploy: 22 | - "pwd" 23 | - "cd $TRAVIS_BUILD_DIR" 24 | - "pwd" 25 | - "tar cjvf moarperf-$(git rev-parse --short HEAD).tar.bz2 lib/*pm6 META6.json README.md service.p6 frontend/node_modules/bootstrap/dist/js/bootstrap.bundle.js{,.map} frontend/node_modules/bootstrap/dist/css/bootstrap{,-reboot,-grid}.css static/*" 26 | - "find . -iname 'MoarPerf*AppImage'" 27 | - "cp ./appimage/out/MoarPerf*.AppImage out/" 28 | 29 | deploy: 30 | provider: releases 31 | file_glob: true 32 | file: 33 | - "moarperf-*.tar.bz2" 34 | - "out/**/*" 35 | - "out/*" 36 | edge: true 37 | draft: true 38 | overwrite: true 39 | skip_cleanup: true 40 | -------------------------------------------------------------------------------- /appimage/do_stuff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ######################################################################## 4 | # Package the binaries built on Travis-CI as an AppImage 5 | # By Simon Peter 2016 6 | # For more information, see http://appimage.org/ 7 | ######################################################################## 8 | 9 | set -xe 10 | 11 | export ARCH="$(arch)" 12 | export VERSION="v0.0.1" 13 | 14 | APP=moarperf 15 | LOWERAPP=$APP 16 | 17 | mkdir -p "$HOME/$APP/$APP.AppDir/usr/" 18 | BUILD_PATH="$(pwd)" 19 | 20 | git clone https://github.com/rakudo/rakudo 21 | cd rakudo 22 | pwd 23 | ls -l 24 | git checkout 2020.10 25 | patch -p1 < ../appimage/raku_skip_home_compunit_repository.patch 26 | sudo perl Configure.pl --prefix="/usr/rakudo/" --gen-moar --relocatable 27 | sudo make -j2 install 28 | 29 | cd ../ 30 | pwd 31 | ls -l 32 | 33 | git clone https://github.com/ugexe/zef 34 | 35 | cd zef 36 | pwd 37 | ls -l 38 | 39 | sudo "/usr/rakudo/bin/raku" -I . bin/zef install . 40 | 41 | cd $BUILD_PATH 42 | 43 | pwd 44 | ls -l 45 | 46 | sudo "/usr/rakudo/bin/raku" "/usr/rakudo/share/perl6/site/bin/zef" install --/test \ 47 | "JSON::Fast" \ 48 | "OO::Monitors" \ 49 | "Cro::HTTP" \ 50 | "Cro::WebSocket" \ 51 | "App::MoarVM::HeapAnalyzer" \ 52 | "Digest::SHA1::Native" \ 53 | "DBIish" 54 | 55 | sudo "/usr/rakudo/bin/raku" "/usr/rakudo/share/perl6/site/bin/zef" install . 56 | 57 | cd "$BUILD_PATH/appimage" 58 | 59 | wget -q https://github.com/AppImage/pkg2appimage/releases/download/continuous/pkg2appimage-1795-x86_64.AppImage -O ./pkg2appimage.AppImage 60 | chmod +x ./pkg2appimage.AppImage 61 | 62 | export VERSION=0.1.1 63 | 64 | mkdir -p ../out/ 65 | ./pkg2appimage.AppImage ./MoarPerf.yml 66 | -------------------------------------------------------------------------------- /frontend/welcome/CodeEditor.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import React from 'react'; 3 | 4 | export default class CodeEditor extends React.Component { 5 | requestBrowse() { 6 | $.ajax({ 7 | url: '/browse', 8 | type: 'POST', 9 | contentType: 'application/json', 10 | data: JSON.stringify({type: "script"}), 11 | success: ({filenames}) => this.setState({suggestedFiles: filenames}) 12 | }); 13 | } 14 | componentDidMount() { 15 | this.requestBrowse(); 16 | } 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | flags: [], 21 | filename: null, 22 | sourcecode: null, 23 | suggestedFiles: [], 24 | args: "", 25 | }; 26 | } 27 | 28 | render() { 29 | let props = this.props; 30 | 31 | let buttonStyle = { 32 | border: "0px", 33 | background: "none" 34 | }; 35 | 36 | return ( 37 |
38 |

Perl 6 scripts in the current directory

39 | 50 |
) 51 | ; 52 | } 53 | }; -------------------------------------------------------------------------------- /appimage/MoarPerf.yml: -------------------------------------------------------------------------------- 1 | app: MoarPerf 2 | 3 | ingredients: 4 | dist: bionic 5 | sources: 6 | - deb http://ftp.fau.de/ubuntu/ bionic main universe 7 | packages: 8 | - libsqlite3-dev 9 | - libssl-dev 10 | - openssl 11 | - libzstd-dev 12 | - libzstd 13 | 14 | script: 15 | - echo "start of recipe script" 16 | - pwd 17 | - ls -l 18 | - echo "will copy rakudo over" 19 | - cp -r /usr/rakudo/ usr/ 20 | - echo "desktop entry" 21 | - mkdir -p usr/share/applications/ 22 | - cp ../../MoarPerf.desktop usr/share/applications/ 23 | - cp ../../MoarPerf.desktop . 24 | - cp ../../MoarPerf.desktop /home/travis/build/timo/moarperf/appimage/MoarPerf/MoarPerf.AppDir/ 25 | - cp ../../MoarPerf.desktop /home/travis/build/timo/moarperf/appimage/MoarPerf/MoarPerf.AppDir/usr/ 26 | - cp ../../stolen_icon.png /home/travis/build/timo/moarperf/appimage/MoarPerf/MoarPerf.AppDir/moarperf_stolen_icon.png 27 | - tree || true 28 | - echo "bin directory and launcher binary" 29 | - mkdir -p usr/bin 30 | - cp ../../moarperf-appimage-launcher /home/travis/build/timo/moarperf/appimage/MoarPerf/MoarPerf.AppDir/usr/bin/ 31 | - sudo mkdir -p /usr/share/applications/ 32 | - sudo cp ../../MoarPerf.desktop /usr/share/applications/ 33 | - echo "folder for the app itself" 34 | - mkdir usr/rakuapp 35 | - cp ../../../META6.json usr/rakuapp 36 | - cp ../../../service.p6 usr/rakuapp 37 | - cp -r ../../../static usr/rakuapp 38 | - cp -r ../../../lib usr/rakuapp 39 | - mkdir -p usr/rakuapp/frontend/node_modules/bootstrap/dist/js/ 40 | - mkdir -p usr/rakuapp/frontend/node_modules/bootstrap/dist/css/ 41 | - cp ../../../frontend/node_modules/bootstrap/dist/js/*.bundle.js* usr/rakuapp/frontend/node_modules/bootstrap/dist/js/ 42 | - cp ../../../frontend/node_modules/bootstrap/dist/css/*.css usr/rakuapp/frontend/node_modules/bootstrap/dist/css/ 43 | - echo "Desktop entry" 44 | - cp ../../MoarPerf.desktop usr/ 45 | -------------------------------------------------------------------------------- /frontend/profiler/components/CallersList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import RoutineList from "./RoutineList"; 3 | 4 | import $ from 'jquery'; 5 | import {RoutineListHeaderComponent} from "./RoutineOverviewPage"; 6 | 7 | class CallersList extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | isLoading: { callers: false }, 12 | callers: null 13 | } 14 | } 15 | 16 | requestCallers() { 17 | this.setState((state) => ({ 18 | isLoading: { 19 | callers: true 20 | } 21 | })); 22 | 23 | const stateChangeForCallers = (self, callers) => { 24 | self.setState((state) => ({ 25 | callers: callers, 26 | isLoading: { callers: false } 27 | })) 28 | } 29 | 30 | $.ajax({ 31 | url: '/routine-callers/' + encodeURIComponent(this.props.routineId), 32 | type: 'GET', 33 | contentType: 'application/json', 34 | success: (callers) => stateChangeForCallers(this, callers), 35 | error: (xhr, errorStatus, errorText) => {this.setState(state => ({isLoading: { callers: false }, allocsError: errorStatus + errorText}))} 36 | }) 37 | } 38 | 39 | componentDidMount() { 40 | this.requestCallers(); 41 | } 42 | 43 | render() { 44 | if (this.state.isLoading.callers || this.state.callers === null) { 45 | return (
Loading, please wait...
); 46 | } 47 | return ( 48 |
49 | 55 |
56 | ); 57 | } 58 | } 59 | 60 | export default CallersList; -------------------------------------------------------------------------------- /frontend/profiler/components/FlameGraph.js: -------------------------------------------------------------------------------- 1 | import Dimensions from "react-dimensions"; 2 | import {FlameGraph} from "react-flame-graph"; 3 | import React from "react"; 4 | 5 | export const AutoSizedFlameGraph = Dimensions()(({containerWidth, height, data, onChange}) => { 6 | console.log("autosized flamegraph with width", containerWidth); 7 | return ( 8 | 9 | ) 10 | }); 11 | 12 | export function postprocessFlameGraphData(data, routineMetadata) { 13 | var depth = 1; 14 | if (typeof routineMetadata === "undefined" || routineMetadata.length == 0) { 15 | return {name: "no data loaded yet", value: 1024}; 16 | } 17 | function recurseLookup(node, curDepth) { 18 | if (curDepth > depth) { 19 | depth = curDepth; 20 | } 21 | if (node.hasOwnProperty('incomplete')) { 22 | return ({ 23 | ...node, 24 | name: (routineMetadata[node.rid] || {name: "?"}).name, 25 | tooltip: node.cid + ": " + (routineMetadata[node.rid] || {name: "?"}).name, 26 | children: node.incomplete ? [{ 27 | name: "[more]", 28 | tooltip: node.cid + " has more children...", 29 | routine_id: node.rid, 30 | value: node.value, 31 | backgroundColor: "#999", 32 | color: "#fff", 33 | }] : [], 34 | incomplete: node.incomplete 35 | }); 36 | } 37 | return ({ 38 | ...node, 39 | tooltip: node.cid + ": " + (routineMetadata[node.rid] || {name: "?"}).name, 40 | name: (routineMetadata[node.rid] || {name: "?"}).name, 41 | children: 42 | node.hasOwnProperty('children') ? 43 | node.children.map(recurseLookup, curDepth + 1) 44 | : [] 45 | }); 46 | } 47 | return {flamegraph: recurseLookup(data, 1), maxDepth: depth}; 48 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moarperf", 3 | "version": "1.0.0", 4 | "description": "MoarVM Performance Analysis Frontend", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "dev": "webpack --display-error-details --mode development", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "flow": "flow", 11 | "cosmos": "cosmos", 12 | "cosmos-export": "cosmos-export" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "babel-preset-env": "^1", 18 | "eslint-config-airbnb": "^16.1.0", 19 | "eslint-plugin-import": "^2.18.2", 20 | "eslint-plugin-jsx-a11y": "^6.2.3", 21 | "eslint-plugin-react": "^7.16.0", 22 | "flow-bin": "^0.76.0", 23 | "webpack": "^4.41.2", 24 | "webpack-cli": "^3.3.9" 25 | }, 26 | "dependencies": { 27 | "@babel/cli": "^7.6.4", 28 | "@babel/core": "^7.6.4", 29 | "@babel/plugin-proposal-class-properties": "^7.5.5", 30 | "@babel/plugin-proposal-function-bind": "^7.2.0", 31 | "@babel/plugin-proposal-object-rest-spread": "^7.6.2", 32 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 33 | "@babel/plugin-transform-modules-commonjs": "^7.6.0", 34 | "@babel/preset-env": "^7.6.3", 35 | "@babel/preset-es2017": "^7.0.0-beta.53", 36 | "@babel/preset-flow": "^7.0.0", 37 | "@babel/preset-react": "^7.6.3", 38 | "@types/react": "^16.9.9", 39 | "acorn": "^6.3.0", 40 | "babel-eslint": "^8.2.6", 41 | "babel-loader": "^8.0.6", 42 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 43 | "babel-preset-stage-0": "^6.24.1", 44 | "bootstrap": "^4.3.1", 45 | "classnames": "^2.2.6", 46 | "eslint": "^4.19.1", 47 | "eslint-plugin-flow": "^2.29.1", 48 | "eslint-plugin-flowtype": "^2.50.3", 49 | "history": "^4.10.1", 50 | "jquery": "^3.4.1", 51 | "memoize-one": "^5.1.1", 52 | "memoize-state": "^1.5.1", 53 | "react": "^16.10.2", 54 | "react-dimensions": "^2.0.0-alpha2", 55 | "react-dom": "^16.10.2", 56 | "react-error-boundary": "^1.2.5", 57 | "react-flame-graph": "^1.3.0", 58 | "react-loadable": "^5.5.0", 59 | "react-redux": "^5.1.2", 60 | "react-router": "^5.1.2", 61 | "react-router-dom": "^5.1.2", 62 | "reactstrap": "^6.5.0", 63 | "recharts": "^1.8.3", 64 | "redux": "^3.7.2", 65 | "redux-thunk": "^2.3.0", 66 | "redux-websocket-action": "^1.0.5", 67 | "safe-buffer": "^5.2.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /frontend/heapanalyzer/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import $ from 'jquery'; 4 | // import type { HeapSnapshotState } from './reducer'; 5 | 6 | export const STATUS_UPDATE = 'HEAP_STATUS_UPDATE'; 7 | export const MODEL_OVERVIEW = 'MODEL_OVERVIEW'; 8 | export const PROGRESS_UPDATE = 'HEAP_PROGRESS_UPDATE'; 9 | export const DATA_UPDATE = 'MODEL_DATA_UPDATE'; 10 | export const SELECTION_SWITCH = 'MODEL_SELECTION_SWITCH'; 11 | 12 | type StatusUpdateAction = { type : "HEAP_STATUS_UPDATE", body: any }; 13 | type ModelOverviewAction = { type : "MODEL_OVERVIEW", suggested_filename: string }; 14 | type ProgressUpdateAction = { type: "HEAP_PROGRESS_UPDATE", 15 | uuid: string, 16 | description: string, 17 | cancellable: boolean, 18 | progress: [number, number, number] 19 | } 20 | type ModelDataAction = { type: "MODEL_DATA_UPDATE", body: any } 21 | type SnapshotSwitchAction = { type: "MODEL_SELECTION_SWITCH", body: number } 22 | 23 | export type HeapSnapshotAction = 24 | StatusUpdateAction | 25 | ModelOverviewAction | 26 | ProgressUpdateAction | 27 | ModelDataAction | 28 | SnapshotSwitchAction; 29 | 30 | // type DispatchType = (HeapSnapshotAction) => void; 31 | // type GetStateType = () => HeapSnapshotState; 32 | 33 | export function requestSnapshot(index : number) { 34 | return (dispatch, getState) => { 35 | $.ajax({ 36 | url: '/request-snapshot', 37 | type: 'POST', 38 | contentType: 'application/json', 39 | data: JSON.stringify({ index }), 40 | success: ({ update_key }) => dispatch( 41 | { 42 | type: "HEAP_STATUS_UPDATE", 43 | body: { 44 | snapshot_index: index, 45 | snapshot_state: { state: "Preparing" }, 46 | update_key 47 | } 48 | }) 49 | }); 50 | if (typeof getState().currentSnapshot !== "undefined") { 51 | dispatch({type: "MODEL_SELECTION_SWITCH", body: index}); 52 | } 53 | }; 54 | } 55 | 56 | export function requestModelData() { 57 | return (dispatch) => { 58 | $.ajax({ 59 | url: '/request-heap-shared-data', 60 | success: (data) => dispatch( 61 | { 62 | type: DATA_UPDATE, 63 | body: { 64 | data 65 | } 66 | }) 67 | }); 68 | }; 69 | } 70 | 71 | export function switchSnapshot(index: number) { 72 | return (dispatch) => dispatch({type: "MODEL_SELECTION_SWITCH", body: index}); 73 | } -------------------------------------------------------------------------------- /frontend/profiler/components/RoutineOverviewPage.jsx: -------------------------------------------------------------------------------- 1 | import ErrorBoundary from 'react-error-boundary'; 2 | import React, { useState, useEffect } from 'react'; 3 | import { Button } from 'reactstrap'; 4 | 5 | import RoutineList from './RoutineList'; 6 | 7 | export const RoutineListHeaderComponent = props => ( 8 | 9 |

Routines

10 | 13 | 16 | 19 | 22 | 25 |
26 | ); 27 | 28 | export default function RoutineOverviewPage(props) { 29 | const [isLoading, setIsLoading] = useState(false); 30 | 31 | useEffect(() => { 32 | if ( 33 | !isLoading && 34 | (props.profilerState.routineOverview.length === 0 || 35 | props.profilerState.routines.length === 0) 36 | ) { 37 | setIsLoading(true); 38 | props.onRequestRoutineOverview(); 39 | } else if ( 40 | isLoading && 41 | props.profilerState.routineOverview.length !== 0 && 42 | props.profilerState.routines.length !== 0 43 | ) { 44 | setIsLoading(false); 45 | } 46 | }, [props.profilerState.routineOverview, props.profilerState.routines]); 47 | 48 | if (isLoading) { 49 | return
Loading, hold on...
; 50 | } 51 | 52 | return ( 53 | 54 | 60 | {props.profilerState.routineOverview.length === 0 || 61 | props.profilerState.routines.length === 0 ? ( 62 | 65 | ) : null} 66 | 76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /service.p6: -------------------------------------------------------------------------------- 1 | use v6.d.PREVIEW; 2 | 3 | use Cro::HTTP::Log::File; 4 | use Cro::HTTP::Server; 5 | use Routes; 6 | use HeapAnalyzerWeb; 7 | use ProfilerWeb; 8 | 9 | with @*ARGS { 10 | if @*ARGS > 0 and (@*ARGS[0] eq "-h" or @*ARGS[0] eq "--help") { 11 | print q:to/CAMELIA/; 12 | ⣀⣴⣶⣿⣿⣿⣷⣶⣤⡀ 13 | ⢀⣼⣿⠟⠋⠁⠀⣀⡀⠈⠻⣿⣦⡀ 14 | ⣾⣿⠁⠀⣤⡶⠛⠉⠛⢷⡄⠈⣿⣿⡄ 15 | ⢸⣿⣿⠀⠀⣿⠀⢠⣶⡄⠘⡿⠀⢸⡿⣿⡄ 16 | ⢸⣿⣿⡀⠀⢸⣦⣘⠛⣃⠜⠁⢠⣿⣷⠘⡇ 17 | ⠘⣿⣿⣧⠀⠀⠛⠉⠉⠀⢀⣴⣿⣿⠇⢠⡇ ⡀ ⢀⣀⣀ ™ 18 | ⠹⣿⣿⣷⡀⠀⠐⣾⣟⠿⠟⠛⢁⣠⣿⠇⠚⡟ ⢀⡄ ⣠⣴⣾⣿⣿⠿⢿⣿⣿⣶⣄⡀ 19 | ⠙⣿⣿⣿⣦⡀⠈⢿⣿⣿⣿⣿⣿⠏ ⢠⠇⡴⠋⠃⣰⣿⡿⠋⠁⠀⠀⠀⠀⠀⠈⠙⠻⣿⣄ 20 | ⣈⣛⣿⣿⣿⣦⣈⡟⢉⣈⡉⢣⣄⣠⠏⣰⠃ ⣼⣿⠋⠀⢀⣤⣶⡄⠈⠻⣿⣷⣦⡄⠈⢻⡆ 21 | ⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⡀⢺⣿⡗⢸⠇⠀⠉⠳⣄⣰⣿⡟⠀⢰⠏⢉⡉⠹⡄⠀⠹⣝⠻⣿⡀⠈⣿ 22 | ⢀⣾⣿⡿⠛⠉⠉⠉⠙⢿⣿⡏⠳⢤⣤⠴⠋⠀⠀⢠⠎⢁⣤⡙⢧⠀⢸⠰⣿⡿⠀⣷⠀⠀⣿⡇⠹⣧⣼⣿ 23 | ⣾⣿⡟⠀⠀⣠⣶⣿⣶⠀⢿⡇⠀⠀⢀⠀⠀⠀⠀⢸⠀⢿⣿⠇⣸⡀⠘⠷⠤⣴⣾⠟⠀⢰⣿⡏⠀⣿⣿⠇ 24 | ⣿⣿⡅⠀⠀⢿⣿⡿⠛⠀⣿⣷⡀⠀⠹⡄⠀⠀⡀⠈⠳⠤⡤⣴⣿⣷⣤⣀⡀⠀⠀⠀⣰⣿⠟⠁⣼⡿⠋ 25 | ⠹⣿⣷⣀⠀⠀⠀⠀⣠⣾⣿⣿⣿⢦⡀⠙⠳⠶⠋⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣶⠿⠋ 26 | ⠙⢿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠁⡞⠈⡷⠲⢤⣤⣤⣶⠟⠛⠉⠛⢿⣿⣿⣿⡉⠉⠉⠉⠉ 27 | ⠈⠉⠛⠛⠛⠋⠉ ⠇ ⡇ ⣿⣿⣿⠏⢰⣿⣷⡄⠀⠹⣿⣿⣧ 28 | ⠁ ⣿⣿⣿⠀⠘⣿⣿⡇⠀⠀⣿⣿⣿ 29 | ⠸⣿⣿⣇⠀⠈⠉⠁⠀⢀⣿⣿⡿ 30 | ⠙⢿⣿⣷⣄⣀⣀⣤⣾⣿⡿⠁ 31 | ⠙⠻⢿⣿⣿⣿⠿⠋ 32 | CAMELIA 33 | print q:to/ABOUT/; 34 | MoarPerf performance analyzer for MoarVM's heap snapshots and profiler sql files 35 | Pass a filename to this script or select the file from the web interface. 36 | See the README at https://github.com/timo/moarperf for more information. 37 | ABOUT 38 | exit; 39 | } 40 | } 41 | 42 | my $heapanalyzer = HeapAnalyzerWeb.new; 43 | my $profiler = ProfilerWeb.new; 44 | my $application = routes($heapanalyzer, $profiler, @*ARGS[0]); 45 | 46 | without @*ARGS[0] { 47 | note "please feel free to pass a filename to service.p6 to open a file immediately." 48 | } 49 | 50 | %*ENV //= do { 51 | note "environment variable MOARPERF_HOST not set. Defaulting to 'localhost'"; 52 | "localhost"; 53 | } 54 | 55 | %*ENV //= do { 56 | note "environment variable MOARPERF_PORT not set. Defaulting to '20000'"; 57 | 20000; 58 | } 59 | 60 | my Cro::Service $http = Cro::HTTP::Server.new( 61 | http => <1.1>, 62 | host => %*ENV, 63 | port => %*ENV, 64 | :$application, 65 | after => [ 66 | Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR) 67 | ] 68 | ); 69 | $http.start; 70 | say "Listening at http://%*ENV:%*ENV"; 71 | 72 | CONTROL { 73 | .perl.say; 74 | } 75 | 76 | react { 77 | whenever signal(SIGINT) { 78 | say "Shutting down..."; 79 | $http.stop; 80 | done; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /frontend/profiler/reducer.js: -------------------------------------------------------------------------------- 1 | import type { ActionTypes } from './actions'; 2 | import {EXPAND_ROUTINE, ROUTINE_CHILDREN_GET, EXPAND_GC_SEQ, GC_SEQ_DETAILS_GET, APP_SET_FULLSCREEN} from "./actions"; 3 | 4 | export type ProfilerState = { 5 | +modelState: string, 6 | +routines: Array 7 | } 8 | 9 | const initialState = { 10 | modelState: 'pre-load', 11 | routines: [], 12 | routineOverview: [], 13 | gc: { 14 | overview: {}, 15 | expanded: {} 16 | }, 17 | expanded: {}, 18 | allRoutineChildren: {}, 19 | filePath: '', 20 | fullscreen: false, 21 | }; 22 | export default function profilerReducer( 23 | state : ProfilerState = initialState, 24 | action : ActionTypes.ProfilerAction, 25 | ) : ProfilerState { 26 | console.log('got a profiler datum to reduce'); 27 | console.log(action); 28 | switch (action.type) { 29 | case 'PROFILE_STATUS_UPDATE': { 30 | console.log('status update, yay'); 31 | const newstate = { ...state }; 32 | if (action.body.data === 'routine_overview') { 33 | newstate.routineOverview = action.body.body; 34 | console.log('routine overview stored'); 35 | } 36 | if (action.body.data === 'all_routines') { 37 | newstate.routines = action.body.body; 38 | console.log('all routines stored'); 39 | } 40 | if (action.body.data === 'gc_overview') { 41 | newstate.gc.overview = action.body.body; 42 | console.log('GC overview stored'); 43 | } 44 | 45 | return newstate; 46 | } 47 | case EXPAND_ROUTINE: { 48 | const newstate = { ...state }; 49 | newstate.expanded = { ...newstate.expanded }; 50 | if (newstate.expanded[action.id]) { 51 | newstate.expanded[action.id] = undefined; 52 | } 53 | else { 54 | newstate.expanded[action.id] = 1; 55 | } 56 | console.log("switched expanded of " + action.id + " ") 57 | return newstate; 58 | } 59 | case EXPAND_GC_SEQ: { 60 | const newstate = { ...state }; 61 | newstate.gc = { ...newstate.gc }; 62 | newstate.gc.expanded = { ...newstate.gc.expanded }; 63 | if (newstate.gc.expanded[action.seq_num]) { 64 | newstate.gc.expanded[action.seq_num] = undefined; 65 | } 66 | else { 67 | newstate.gc.expanded[action.seq_num] = 1; 68 | } 69 | console.log("switched expanded of gc " + action.seq_num + " ") 70 | return newstate; 71 | } 72 | case ROUTINE_CHILDREN_GET: { 73 | const newstate = { ...state }; 74 | newstate.allRoutineChildren[action.id] = action.entries; 75 | return newstate; 76 | } 77 | case GC_SEQ_DETAILS_GET: { 78 | const newstate = { ...state }; 79 | newstate.gc = { ...state.gc }; 80 | newstate.gc.seq_details = { ...state.gc.seq_details }; 81 | newstate.gc.seq_details[action.seq_num] = action.data.stats_of_sequence; 82 | return newstate; 83 | } 84 | default: { 85 | return { ...state }; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rakudo Profiler Front-End 2 | 3 | Moarperf is a tool that takes the output of the Rakudo Perl 6 profilers available on MoarVM and makes them viewable with a web browser. 4 | 5 | Depending on whether you pass an "instrumented profiler" file (`.sql`) or a "heap snapshot file" (`.mvmheap`), you will get one or the other profiler frontend. 6 | 7 | Running your perl6 program with `--profile=foo.sql` or `--profile=bar.mvmheap` will generate a file for you. Additionally, the `Telemetry` module that comes with Rakudo offers a `snap` sub that takes a `:heap` argument that lets you create heap snapshots at specific points in your program, rather than whenever the GC runs. 8 | 9 | # Installing the Front-end 10 | 11 | ## AppImage 12 | 13 | There is a version of moarperf for linux that is packaged as an AppImage, which is a single file that can be executed directly. It contains a full rakudo of its own and does not require any kind of installation. You can find it on [the "releases" page of the moarperf repo](https://github.com/timo/moarperf/releases/). 14 | 15 | ## Traditional Installation 16 | 17 | The Perl 6/Raku part of the program has some dependencies that can be installed with `zef`. The command to do that is `zef install --depsonly .` - but if you want it a bit faster, you can `--/tests` to skip testing modules before installation. 18 | 19 | The javascript portion of the program has - like any javascript application seems to, nowadays - a boatload of dependencies. That's why there's pre-built packages up on github that have the javascript portion already "compiled". You can find them on [the "releases" page of the moarperf repo](https://github.com/timo/moarperf/releases/) 20 | 21 | # Building the front-end javascript code from source 22 | 23 | Start with a clone of the repository. There should be a `frontend` folder with a `package.json` file, which is what `npm` and friends work with. Change into the `frontend` folder and run `npm install .`, which will download a whole lot of javascript packages. There are often some errors or warnings, but they can mostly be ignored. 24 | 25 | Finally, compile the frontend code with the command `npm run build`. After it outputs a colorful list of files with file sizes and such, but it's not exiting, the `webpack.config.js` may still have `watch: true` turned on, in which case the build script will keep running and check for changes you make to the source files to immediately recompile. 26 | 27 | 28 | # Running the front-end 29 | 30 | After installing the perl6/raku dependencies and either extracting the release tarball, or building the javascript code from source, you can run `perl6 -I . service.p6` in the root folder to start the program. That is where `META6.json` lives. By default it will offer a web interface on http://localhost:20000, but environment variables `MOARPERF_HOST` and `MOARPERF_PORT` can be used to change that. Passing a filename, either a `.sql`, a `.sqlite3`, or a `.mvmheap` file, will immediately load the data in question. 31 | 32 | # More info 33 | 34 | My blog on https://wakelift.de has a couple of posts that explain aspects of the program. 35 | 36 | The program's development is funded by a grant from The Perl Foundation. 37 | -------------------------------------------------------------------------------- /frontend/heapanalyzer/components/SnapshotList.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | 3 | import { FormGroup, Form, Label, Input, Button, ButtonGroup } from 'reactstrap'; 4 | 5 | /* TODO: put this in a proper module of its own */ 6 | export function numberFormatter(number, fractionDigits = 0, thousandSeperator = ',', fractionSeperator = '.') { 7 | if (number!==0 && !number || !Number.isFinite(number)) return number; 8 | const frDigits = Number.isFinite(fractionDigits)? Math.min(Math.max(fractionDigits, 0), 7) : 0; 9 | const num = number.toFixed(frDigits).toString(); 10 | 11 | const parts = num.split('.'); 12 | let digits = parts[0].split('').reverse(); 13 | let sign = ''; 14 | if (num < 0) {sign = digits.pop()} 15 | let final = []; 16 | let pos = 0; 17 | 18 | while (digits.length > 1) { 19 | final.push(digits.shift()); 20 | pos++; 21 | if (pos % 3 === 0) {final.push(thousandSeperator)} 22 | } 23 | final.push(digits.shift()); 24 | return `${sign}${final.reverse().join('')}${frDigits > 0 ? fractionSeperator : ''}${frDigits > 0 && parts[1] ? parts[1] : ''}` 25 | } 26 | 27 | type SnapshotListProps = { 28 | modelState: "post-load", 29 | currentSnapshot: number, 30 | loadedSnapshots: Array, 31 | onRequestSnapshot: (number) => void, 32 | onSwitchSnapshot: (number) => void, 33 | operations: Array, 34 | highscores: any, 35 | } | { 36 | modelState: "pre-load" 37 | } 38 | 39 | export default function SnapshotList(props : SnapshotListProps) { 40 | let [requestedSnapshot, setRequestedSnapshot] = useState(""); 41 | 42 | if (props.modelState === 'post-load') { 43 | return [ 44 |
45 |
{ props.onRequestSnapshot(parseInt(requestedSnapshot)); ev.preventDefault() }}> 46 | 47 | 48 | setRequestedSnapshot(ev.target.value)} value={requestedSnapshot}/> 49 | 50 |
51 |
52 | Snapshots: 53 | { 54 | props.loadedSnapshots.map(({ state, update_key }, index) => { 55 | let hasUpdateWidget = typeof update_key === "string" 56 | && typeof props.operations[update_key] !== "undefined"; 57 | let interestingProgress = hasUpdateWidget && props.operations[update_key].progress[2] < 100; 58 | let updateWidget = interestingProgress 59 | ? { props.operations[update_key].progress[2].toFixed(0) }% 60 | : <>; 61 | 62 | if (state !== "Unprepared") { 63 | return ( 64 | 68 | ); 69 | } 70 | }) 71 | } 72 | 73 |
74 |
75 | ]; 76 | } else if (props.modelState === 'pre-load') { 77 | return
Please wait for the model file to be loaded
; 78 | } 79 | return
What is a {props.modelState}??
; 80 | }; 81 | -------------------------------------------------------------------------------- /frontend/heapanalyzer/reducer.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import * as ActionTypes from './actions'; 4 | 5 | export type LoadedSnapshotState = { 6 | state: string, 7 | progress: Array, 8 | } 9 | 10 | export type OperationHandle = { 11 | uuid: string, 12 | description: string, 13 | progress: [number, number, number], 14 | cancellable: boolean, 15 | } 16 | 17 | export type HeapSnapshotState = { 18 | +loadedSnapshots: Array, 19 | +filePath: string, 20 | +modelState: string, 21 | +fileIsLoaded: boolean, 22 | currentSnapshot: number, 23 | summaries: any, 24 | highscores: any, 25 | modelData: {types: {[number]: {name: string, repr: string} }, frames: {[number]: any}}, 26 | 27 | runningOperations: { [string]: OperationHandle }, 28 | } 29 | 30 | const initialState = { 31 | modelState: 'unknown', 32 | loadedSnapshots: [], 33 | filePath: '', 34 | runningOperations: {}, 35 | }; 36 | export default function heapAnalyzerReducer( 37 | state : HeapSnapshotState = initialState, 38 | action : ActionTypes.HeapSnapshotAction, 39 | ) : HeapSnapshotState { 40 | console.log('heap: got a thing to reduce'); 41 | console.log(action.body); 42 | switch (action.type) { 43 | case ActionTypes.STATUS_UPDATE: { 44 | console.log(" it's a status update"); 45 | if (action.body.hasOwnProperty('model_state')) { 46 | console.log(' updating the status with model state'); 47 | console.log(action.body.loaded_snapshots); 48 | return { 49 | ...state, 50 | modelState: action.body.model_state, 51 | loadedSnapshots: action.body.loaded_snapshots, 52 | numSnapshots: action.body.num_snapshots, 53 | summaries: action.body.summaries, 54 | highscores: action.body.highscores, 55 | }; 56 | } else if (action.body.hasOwnProperty('snapshot_index')) { 57 | const newSnapshots = state.loadedSnapshots.slice(); 58 | console.log(' changing snapshot at', action.body.snapshot_index); 59 | console.log(action); 60 | newSnapshots[action.body.snapshot_index] = { 61 | ...action.body.snapshot_state, 62 | update_key: typeof action.body.update_key === "undefined" ? newSnapshots[action.body.snapshot_index].update_key : action.body.update_key 63 | }; 64 | console.log(newSnapshots); 65 | return { 66 | ...state, 67 | loadedSnapshots: newSnapshots, 68 | }; 69 | } 70 | 71 | console.log("didn't understand this"); 72 | console.log(action); 73 | return state; 74 | } 75 | case ActionTypes.MODEL_OVERVIEW: { 76 | console.log('model overview!'); 77 | console.log(action); 78 | return { 79 | ...state, 80 | 81 | filePath: action.suggested_filename, 82 | }; 83 | } 84 | case ActionTypes.PROGRESS_UPDATE: { 85 | console.log("progress update"); 86 | return { 87 | ...state, 88 | runningOperations: { 89 | ...state.runningOperations, 90 | [action.body.uuid]: { 91 | description: (typeof action.body.description === "undefined") 92 | ? state.runningOperations[action.body.uuid].description 93 | : action.body.description, 94 | cancellable: action.body.cancellable, 95 | progress: action.body.progress, 96 | } 97 | } 98 | } 99 | } 100 | case ActionTypes.SELECTION_SWITCH: 101 | console.log("switch selected snapshot"); 102 | return { 103 | ...state, 104 | currentSnapshot: action.body, 105 | }; 106 | case ActionTypes.DATA_UPDATE: 107 | console.log("get heap shared data"); 108 | return { 109 | ...state, 110 | modelData: action.body.data, 111 | }; 112 | default: 113 | (action: empty); 114 | console.log("didn't understand this action"); 115 | return state; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /frontend/profiler/components/AllocationParts.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {numberFormatter} from "./RoutinePieces"; 3 | import {Bytes} from "./AllocationViewer"; 4 | 5 | export const isFromRakudoCore = (scdesc, name) => ( 6 | (scdesc === "gen/moar/CORE.setting" 7 | || scdesc === "gen/moar/CORE.c.setting" 8 | || scdesc === "gen/moar/CORE.d.setting" 9 | || scdesc === "gen/moar/CORE.e.setting" 10 | || scdesc === "gen/moar/BOOTSTRAP.nqp" 11 | || scdesc === "gen/moar/stage2/NQPHLL.nqp" 12 | || scdesc === "gen/moar/stage2/NQPCORE.nqp" 13 | || scdesc === "gen/moar/stage2/NQPCORE.setting" 14 | || scdesc === "gen/moar/stage2/QRegex.nqp") 15 | && !name.startsWith("BOOT") && !name.startsWith("NQP") 16 | && !name.startsWith("Rakudo::Iterator") && !name.startsWith("Rakudo::Internals") 17 | ) 18 | 19 | export function AllocNameAndRepr({alloc}) { 20 | return ({alloc.name} 21 | {isFromRakudoCore(alloc.scdesc, alloc.name) ? function () { 22 | return ( 23 | 24 | {" "}Docs 25 | 26 | ) 27 | }() : ""}
28 | {alloc.repr !== "P6opaque" ? {alloc.repr} : ""}) 29 | } 30 | 31 | export function AllocTableContent({allocations, parentSpeshJitEntries = 0, parentBareEntries = 0, parentSites = 0}) { 32 | 33 | return allocations.map((alloc) => { 34 | const bareAllocs = alloc.count - alloc.jit - alloc.spesh; 35 | return ( 36 | 37 | { 38 | parentSites !== 0 && typeof alloc.participants !== "undefined" && 39 | 40 | {alloc.participants.split(",").length} / {parentSites} sites 41 | 42 | } 43 | 44 | 45 | 46 | 47 | { 48 | parentSpeshJitEntries === 0 && parentBareEntries !== 0 49 | ? 50 | 51 | {numberFormatter(bareAllocs)}× 52 | {parentBareEntries > 0 && 53 |
54 | {numberFormatter(bareAllocs / (parentBareEntries - parentSpeshJitEntries), 2)}× per 55 | entry 56 |
} 57 | 58 | : 59 | 60 | 61 | {numberFormatter(bareAllocs)}× before spesh 62 | {parentBareEntries > 0 && 63 |
64 | {numberFormatter(bareAllocs / (parentBareEntries - parentSpeshJitEntries), 2)}× per 65 | regular entry 66 |
} 67 | 68 | 69 | {numberFormatter(alloc.spesh + alloc.jit)}× after spesh/jit 70 | {parentSpeshJitEntries > 0 && 71 |
72 | {numberFormatter((alloc.spesh + alloc.jit) / parentSpeshJitEntries, 2)}× per 73 | spesh/jit 74 | entry 75 | 76 |
77 | } 78 | 79 |
80 | } 81 | 82 | ); 83 | }) 84 | } 85 | 86 | -------------------------------------------------------------------------------- /frontend/profiler/actions.js: -------------------------------------------------------------------------------- 1 | 2 | // @flow 3 | 4 | import $ from 'jquery'; 5 | import type { ProfilerState } from './reducer'; 6 | import {FILE_REQUESTED} from "../welcome/actions"; 7 | 8 | export const CHANGE_FILE_PATH = 'CHANGE_FILE_PATH'; 9 | export const EXPAND_ROUTINE = 'EXPAND_ROUTINE'; 10 | export const ROUTINE_CHILDREN_GET = 'ROUTINE_CHILDREN_GET'; 11 | export const GC_OVERVIEW_GET = 'GC_OVERVIEW_GET'; 12 | export const EXPAND_GC_SEQ= 'EXPAND_GC_SEQ'; 13 | export const GC_SEQ_DETAILS_GET = 'GC_SEQ_DETAILS_GET'; 14 | 15 | 16 | type FilePathChangedAction = { type: "CHANGE_FILE_PATH", text: string }; 17 | type RoutineChildrenGetAction = { type: "ROUTINE_CHILDREN_GET", id: number, entries: Array<{}> }; 18 | type ExpandRoutineAction = { type: "EXPAND_ROUTINE", id: number }; 19 | type GcOverviewGetAction = { type: "GC_OVERVIEW_GET", data: any}; 20 | type GcSeqExpandAction = { type: "EXPAND_GC_SEQ", seq_num: number }; 21 | type GcSeqDetailsGetAction = { type: "GC_SEQ_DETAILS_GET", seq_num: number, data: any }; 22 | 23 | export type ProfilerAction = FilePathChangedAction | 24 | RoutineChildrenGetAction | 25 | ExpandRoutineAction | 26 | GcOverviewGetAction | 27 | GcSeqExpandAction | 28 | GcSeqDetailsGetAction 29 | ; 30 | //StatusUpdateAction | 31 | //ModelOverviewAction | 32 | //FilePathChangedAction; 33 | 34 | type DispatchType = (ProfilerAction) => void; 35 | type GetStateType = () => ProfilerState; 36 | 37 | 38 | export function expandRoutine(id : number) { 39 | return (dispatch : DispatchType, getState : GetStateType) => { 40 | dispatch({ 41 | type: EXPAND_ROUTINE, 42 | id: id 43 | }); 44 | console.log("will we get data about", id, "?", getState().profiler.expanded[id], typeof getState().profiler.allRoutineChildren[id]); 45 | if (getState().profiler.expanded[id] && typeof getState().profiler.allRoutineChildren[id] === "undefined") { 46 | $.ajax({ 47 | url: '/routine-children/' + encodeURIComponent(id), 48 | type: 'GET', 49 | contentType: 'application/json', 50 | success: (entries) => dispatch({type: ROUTINE_CHILDREN_GET, id, entries}), 51 | }); 52 | } 53 | } 54 | } 55 | 56 | export function getGCOverview() { 57 | return (dispatch : DispatchType, getState : GetStateType) => { 58 | if (typeof getState().profiler.gcOverview === "undefined") { 59 | $.ajax({ 60 | url: '/gc-overview', 61 | type: 'GET', 62 | contentType: 'application/json', 63 | success: (data) => dispatch({type: 'PROFILE_STATUS_UPDATE', body: {data: "gc_overview", body: data}}), 64 | }); 65 | } 66 | } 67 | } 68 | 69 | export function getRoutineOverview() { 70 | return (dispatch : DispatchType, getState : GetStateType) => { 71 | if (typeof getState().profiler.routines === "undefined" || getState().profiler.routines.length === 0) { 72 | $.ajax({ 73 | url: '/all-routines', 74 | type: 'GET', 75 | contentType: 'application/json', 76 | success: (data) => dispatch({type: 'PROFILE_STATUS_UPDATE', body: {data: "all_routines", body: data}}), 77 | }); 78 | } 79 | if (typeof getState().profiler.routineOverview === "undefined" || getState().profiler.routineOverview.length === 0) { 80 | $.ajax({ 81 | url: '/routine-overview', 82 | type: 'GET', 83 | contentType: 'application/json', 84 | success: (data) => dispatch({type: 'PROFILE_STATUS_UPDATE', body: {data: "routine_overview", body: data}}), 85 | }); 86 | } 87 | } 88 | } 89 | 90 | export function getGCDetails(seq_num : number) { 91 | return (dispatch : DispatchType, getState : GetStateType) => { 92 | dispatch({ 93 | type: EXPAND_GC_SEQ, 94 | seq_num: seq_num 95 | }); 96 | if (typeof getState().profiler.gcDetails === "undefined") { 97 | $.ajax({ 98 | url: '/gc-details/' + encodeURIComponent(seq_num), 99 | type: 'GET', 100 | contentType: 'application/json', 101 | success: (data) => dispatch({type: 'GC_SEQ_DETAILS_GET', seq_num, data}), 102 | }); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /frontend/welcome/GreetingsPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, CardDeck, Card, CardTitle, CardText, Row, Col } from 'reactstrap'; 3 | import CodeEditor from './CodeEditor'; 4 | 5 | export default class GreetingsPage extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | interest: 'everything', 10 | step: 'start', 11 | profileKind: 'measurePerformance', 12 | }; 13 | } 14 | 15 | render() { 16 | const props = this.props; 17 | 18 | const 19 | buttonStyle = { 20 | padding: '1em', 21 | border: '1px solid black', 22 | }; 23 | const 24 | currentButtonStyle = { 25 | padding: '1em', 26 | border: '1px solid darkblue', 27 | background: 'lightblue', 28 | }; 29 | const 30 | iconStyle = { 31 | display: 'block', 32 | }; 33 | const 34 | greetingsPanelStyle = { 35 | display: 'grid', 36 | gridTemplateAreas: 37 | `"leftPanel leftPanel rightPanel" 38 | "leftPanel leftPanel ." 39 | "confirmation confirmation confirmation" 40 | "bottomPanel bottomPanel bottomPanel"`, 41 | gridTemplateColumns: '1fr 1fr 1fr', 42 | }; 43 | const 44 | interestPanelStyle = { 45 | gridArea: 'rightPanel', 46 | margin: '1em', 47 | }; 48 | const 49 | intentPanelStyle = { 50 | gridArea: 'leftPanel', 51 | margin: '1em', 52 | display: 'grid', 53 | gridGap: '1em', 54 | }; 55 | 56 | const 57 | IconButton = (buttonProps) => { 58 | const isSelected = this.state.profileKind === buttonProps.selectionValue; 59 | return (); 66 | }; 67 | 68 | const isIntentShown = () => this.state.step === 'start'; 69 | 70 | const 71 | LeftPanel = (props) => { 72 | return isIntentShown() ? 73 | (

I would like to 74 | ... 75 |

76 | 77 | 78 | Measure performance 79 | Run Perl 6 code and measure performance. 80 | 85 | 86 | 87 | Measure memory usage 88 | Run Perl 6 code and measure memory usage. 89 | 90 | 91 | 92 | Analyze results 93 | Open the result file from an earlier run for inspection 94 | 95 | 96 | 97 | ) 98 | : (Nope); 99 | }; 100 | const handleRadioClick = event => this.setState({ interest: event.target.value }); 101 | return [ 102 | 103 | 104 |

I am interested in ...

105 |
the performance of my program
106 |
the performance of rakudo
107 |
all available data
108 |
, 109 | 110 | 111 | ]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /frontend/profiler/components/AllocationTypeList.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { Table, Button } from 'reactstrap'; 3 | import ErrorBoundary from 'react-error-boundary'; 4 | import Routine from './Routine'; 5 | 6 | 7 | export default class AllocationTypeList extends Component { 8 | static defaultProps = { 9 | expanded: [], 10 | HeaderComponent: ({}) => (

Routines

), 11 | defaultSort: ((a, b) => b.exclusive_time - a.exclusive_time), 12 | } 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | sortTarget: "default", 18 | sortInvert: false, 19 | displayAmount: 100, 20 | filter: {}, 21 | } 22 | } 23 | 24 | changeFilter(filter) { 25 | this.setState((state) => {filter: filter}) 26 | } 27 | 28 | changeSorting(sortTarget) { 29 | this.setState( 30 | (state) => ({ 31 | sortTarget: sortTarget, 32 | sortInvert: sortTarget === state.sortTarget ? !state.sortInvert : false 33 | }) 34 | ) 35 | } 36 | 37 | render() { 38 | let { 39 | routines, 40 | metadata, 41 | expanded, 42 | allRoutineChildren, 43 | columns, 44 | maxTime, 45 | parentEntries, 46 | onExpandButtonClicked, 47 | HeaderComponent, 48 | defaultSort, 49 | filterFunction, 50 | } = this.props; 51 | 52 | if (typeof columns === "string") { 53 | columns = columns.split(" "); 54 | } 55 | const nameMapping = { 56 | expand: "", 57 | sitecount: "Sites", 58 | nameInfo: "Name", 59 | entriesInfo: "Entries", 60 | exclusiveInclusiveTime: "Time", 61 | inlineInfo: "Inlined", 62 | }; 63 | const styleMapping = { 64 | expand: {width: "7%"}, 65 | sitecount: {width: "10%"}, 66 | entriesInfo: {width: "15%"}, 67 | inlineInfo: {width: "10%"}, 68 | }; 69 | const sortFunc = defaultSort; 70 | const filtered = filterFunction === null || typeof filterFunction === "undefined" ? Array.from(routines) : routines.filter(filterFunction); 71 | const preSortedRoutines = filtered.sort( 72 | this.state.sortInvert ? (a, b) => sortFunc(b, a) : sortFunc 73 | ); 74 | 75 | const sortedRoutines = preSortedRoutines.slice(0, this.state.displayAmount); 76 | 77 | const byInclusiveTime = typeof maxTime === "undefined" ? Array.from(routines).map(r => r.inclusive_time).sort((a, b) => a - b) : []; 78 | const myMaxTime = typeof maxTime === "undefined" ? byInclusiveTime.pop() : maxTime; 79 | console.log(maxTime, "is the max time."); 80 | const loadMoreRoutines = () => self.setState(state => ({displayAmount: state.displayAmount + 100 })); 81 | return 82 | 92 | 93 | 94 | 95 | {columns.map((txt) => ())} 96 | 97 | 98 | 99 | { 100 | sortedRoutines.map((routine) => 101 | ( 102 | )) 113 | } 114 | { 115 | sortedRoutines.length < preSortedRoutines.length && 116 | 117 | 118 | 119 | } 120 | 121 |
{nameMapping[txt]}
Showing {sortedRoutines.length } of { preSortedRoutines.length } routines.
122 |
; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /frontend/profiler/components/RoutineList.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table, Button } from 'reactstrap'; 3 | import ErrorBoundary from 'react-error-boundary'; 4 | 5 | import Routine from './Routine'; 6 | 7 | 8 | export default class RoutineList extends Component { 9 | static defaultProps = { 10 | expanded: [], 11 | columns: 'expand sitecount nameInfo entriesInfo exclusiveInclusiveTime', 12 | HeaderComponent: () => (

Routines

), 13 | defaultSort: 'exclusive_time', 14 | } 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | sortTarget: props.defaultSort, 20 | sortInvert: false, 21 | displayAmount: 100, 22 | filter: {}, 23 | }; 24 | this.changeFilter = this.changeFilter.bind(this); 25 | this.changeSorting = this.changeSorting.bind(this); 26 | } 27 | 28 | changeFilter(filter) { 29 | this.setState((state) => { filter; }); 30 | } 31 | 32 | changeSorting(sortTarget) { 33 | this.setState(state => ({ 34 | sortTarget, 35 | sortInvert: sortTarget === state.sortTarget ? !state.sortInvert : false, 36 | })); 37 | } 38 | 39 | render() { 40 | let { 41 | routines, 42 | metadata, 43 | expanded, 44 | allRoutineChildren, 45 | columns, 46 | maxTime, 47 | parentEntries, 48 | onExpandButtonClicked, 49 | HeaderComponent, 50 | defaultSort, 51 | filterFunction, 52 | shouldScrollTo, 53 | } = this.props; 54 | 55 | const self = this; 56 | 57 | if (typeof columns === 'string') { 58 | columns = columns.split(' '); 59 | } 60 | const nameMapping = { 61 | expand: '', 62 | sitecount: 'Sites', 63 | nameInfo: 'Name', 64 | entriesInfo: 'Entries', 65 | exclusiveInclusiveTime: 'Time', 66 | inlineInfo: 'Inlined', 67 | }; 68 | const styleMapping = { 69 | expand: { width: '7%' }, 70 | sitecount: { width: '10%' }, 71 | entriesInfo: { width: '15%' }, 72 | inlineInfo: { width: '10%' }, 73 | }; 74 | 75 | const attrOfEither = (attr, a, b) => { 76 | if (a !== undefined) { 77 | if (a.hasOwnProperty(attr)) { return a[attr]; } 78 | } 79 | if (b !== undefined) { 80 | if (b.hasOwnProperty(attr)) { return b[attr]; } 81 | } 82 | return 0; 83 | }; 84 | 85 | const comparify = (a, b) => 86 | ((typeof a) === 'string' || (typeof b) === 'string' 87 | ? a.toString().localeCompare(b.toString()) 88 | : b - a); 89 | 90 | const sortFunc = 91 | (a, b) => 92 | comparify( 93 | attrOfEither(self.state.sortTarget, a, metadata[b.id]), 94 | attrOfEither(self.state.sortTarget, b, metadata[a.id]), 95 | ); 96 | 97 | const filtered = filterFunction === null || typeof filterFunction === 'undefined' 98 | ? Array.from(routines) 99 | : routines.filter(filterFunction); 100 | 101 | const preSortedRoutines = filtered.sort(this.state.sortInvert ? (a, b) => sortFunc(b, a) : sortFunc); 102 | 103 | const sortedRoutines = preSortedRoutines.slice(0, this.state.displayAmount); 104 | 105 | const byInclusiveTime = typeof maxTime === 'undefined' ? Array.from(routines).map(r => r.inclusive_time).sort((a, b) => a - b) : []; 106 | const myMaxTime = typeof maxTime === 'undefined' ? byInclusiveTime.pop() : maxTime; 107 | console.log(maxTime, 'is the max time.'); 108 | const loadMoreRoutines = () => self.setState(state => ({ displayAmount: state.displayAmount + 100 })); 109 | 110 | return ( 111 | 112 | 122 | 123 | 124 | 125 | {columns.map(txt => ())} 126 | 127 | 128 | 129 | { 130 | sortedRoutines.map(routine => ( 131 | 132 | 144 | 145 | )) 146 | } 147 | { 148 | sortedRoutines.length < preSortedRoutines.length && 149 | 150 | 151 | 155 | 156 | 157 | } 158 | 159 |
{nameMapping[txt]}
152 | Showing {sortedRoutines.length } of { preSortedRoutines.length } routines. 153 | 154 |
160 |
161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /frontend/profiler/components/SpeshOverview.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import {Breadcrumb, BreadcrumbItem, Button, Col, Container, Row, Table, Input} from 'reactstrap'; 3 | import {Link, Redirect} from 'react-router-dom'; 4 | import $ from 'jquery'; 5 | import classnames from 'classnames'; 6 | import ErrorBoundary from 'react-error-boundary' 7 | import {EntriesInfo, RoutineNameInfo, numberFormatter} from "./RoutinePieces"; 8 | 9 | const numberOrNothing = value => ( 10 | value === 0 ? - : numberFormatter(value) 11 | ) 12 | 13 | export default function SpeshOverview(props) { 14 | const [routineData, setRoutineData] = useState(null); 15 | useEffect( 16 | () => { 17 | $.ajax({ 18 | url: '/routine-spesh-overview', 19 | type: 'GET', 20 | contentType: 'application/json', 21 | success: data => setRoutineData({ok: true, data }), 22 | error: error => setRoutineData({ok: false, error }) 23 | }); 24 | }, [] 25 | ); 26 | 27 | useEffect( 28 | () => { 29 | if (typeof props.metadata === "undefined" || props.metadata.length === 0) 30 | props.onRequestRoutineOverview(); 31 | }, [props.metadata] 32 | ) 33 | 34 | const makeRoutineRow = routine => { 35 | console.log(routine, props.metadata[routine.id]); 36 | return ( 37 | 38 | {numberFormatter(routine.sites)} 39 | 40 | 41 | {numberOrNothing(routine.deopt_one)} 42 | {numberOrNothing(routine.deopt_all)} 43 | {numberOrNothing(routine.osr)} 44 | 45 | ); 46 | }; 47 | 48 | const routineListPart = 49 | routineData === null 50 | || typeof props.metadata === "undefined" 51 | || props.metadata.length === 0 ? 52 | <>Loading... 53 | : routineData.ok === true ? 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | { 67 | routineData.data.map(makeRoutineRow) 68 | } 69 | 70 |
SitesRoutineEntriesDeopt OneDeopt AllOSR
71 | : <>Error: { routineData.error } 72 | 73 | return ( 74 | 75 | 76 | 77 | { 78 | routineListPart 79 | } 80 | 81 | 82 | 83 | 84 |

Specializer Performance

85 |

86 | MoarVM comes with a dynamic code optimizer called "spesh". 87 | It makes your code faster by observing at run time what 88 | types are used where, what methods end up being called in 89 | certain situations where there are multiple potential 90 | candidates, and so on. This is called specialization, because 91 | it creates versions of the code that take shortcuts based 92 | on assumptions it made from the observed data. 93 |

94 |

Deoptimization

95 |

96 | Assumptions, however, are there to be broken. Sometimes 97 | the optimized and specialized code finds that an 98 | assumption no longer holds. Parts of the specialized 99 | code that detect this are called "guards". When a guard 100 | detects a mismatch, the running code has to be switched 101 | from the optimized code back to the unoptimized code. 102 | This is called a "deoptimization", or "deopt" for 103 | short. 104 |

105 |

106 | Deopts are a natural part of a program's life, and at 107 | low numbers they usually aren't a problem. For example, 108 | code that reads data from a file would read from a 109 | buffer most of the time, but at some point the buffer 110 | would be exhausted and new data would have to be 111 | fetched from the filesystem. This could mean a deopt. 112 |

113 |

114 | If, however, the profiler points out a large amount of 115 | deopts, there could be an optimization opportunity. 116 |

117 |

On-Stack Replacement (OSR)

118 |

119 | Regular optimization activates when a function is 120 | entered, but programs often have loops that run for 121 | a long time until the containing function is entered 122 | again. 123 |

124 |

125 | On-Stack Replacement is used to handle cases like this. 126 | Every round of the loop in the unoptimized code will 127 | check if an optimized version can be entered. This has 128 | the additional effect that a deoptimization in such 129 | code can quickly lead back into optimized code. 130 |

131 |

132 | Situations like these can cause high numbers of deopts 133 | along with high numbers of OSRs. 134 |

135 | 136 |
137 |
138 | ) 139 | } -------------------------------------------------------------------------------- /frontend/heapanalyzer/components/Graphs.jsx: -------------------------------------------------------------------------------- 1 | import {Bar, BarChart, CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; 2 | import React from "react"; 3 | import {numberFormatter} from "./SnapshotList"; 4 | 5 | type SnapshotIndex = number; 6 | 7 | type HighscoreColumnData = { [string]: number }; 8 | type HighscoreInputData = [ 9 | { 10 | frames_by_size: Array, 11 | frames_by_count: Array, 12 | types_by_size: Array, 13 | types_by_count: Array, 14 | frame_details: { [number]: { cuid: string, name: string, file: string, line: number } }, 15 | type_details: { [number]: { name: string, repr: string } } 16 | } 17 | ]; 18 | type LineKey = string; 19 | type HighscoreGraphData = { 20 | frames_by_size: Array<{ 21 | snapshot: SnapshotIndex, 22 | [LineKey]: number, 23 | }> 24 | }; 25 | 26 | export function HighscoreLineChart({highscores, dataKey: key, numberOfLines = 5}) { 27 | let data = highscores[key]; 28 | var allKeys = {}; 29 | var scorePerKey = {}; 30 | 31 | if (typeof data === "undefined") { 32 | return <> 33 | } 34 | 35 | for (let entry of data) { 36 | for (let innerKey in entry) { 37 | allKeys[innerKey] = 1; 38 | if (typeof scorePerKey[innerKey] === "undefined") { 39 | scorePerKey[innerKey] = 0; 40 | } 41 | scorePerKey[innerKey] += entry[innerKey]; 42 | } 43 | } 44 | 45 | var keyList = []; 46 | for (let key in allKeys) { 47 | keyList.push(key); 48 | } 49 | 50 | keyList.sort((a, b) => (scorePerKey[b] - scorePerKey[a])); 51 | 52 | keyList = keyList.slice(0, numberOfLines); 53 | 54 | let startValue = 5; 55 | let endValue = 80; 56 | let valueStep = (endValue - startValue) / numberOfLines; 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | { 65 | keyList.map((key, index) => ( 66 | 68 | )) 69 | } 70 | { 71 | const outer = stuff.payload; 72 | return ( 73 |
74 |
    75 | { 76 | outer.map(val => (
  • {val.name}: {numberFormatter(val.value)}
  • )) 77 | } 78 |
79 |
80 | ); 81 | }}/> 82 |
83 |
84 | ); 85 | } 86 | 87 | export function HighscoreGraphs(props: { value: number, onChange: (e: any) => void, highscores: any }) { 88 | return
89 |

Top scores

90 | { 91 | typeof props.highscores === "undefined" 92 | ?
Waiting for results...
93 | : 94 | <> 95 |

Top objects by size

96 | 98 |

Top objects by count

99 | 101 |

Top frames by size

102 | 104 |

Top frames by count

105 | 107 | 108 | } 109 |
; 110 | } 111 | 112 | export function SummaryGraphs(props: { data: any }) { 113 | return

Summaries

114 |

Total Heap Size

115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |

Total Object Count

124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |

Typeobjects, STables, and Frames

133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 |

References

144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 |
; 152 | } -------------------------------------------------------------------------------- /frontend/profiler/components/RoutinePieces.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { Container, Button, Nav, NavItem, NavLink } from 'reactstrap'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | export function numberFormatter(number, fractionDigits = 0, thousandSeperator = ',', fractionSeperator = '.') { 6 | if (number!==0 && !number || !Number.isFinite(number)) return number 7 | const frDigits = Number.isFinite(fractionDigits)? Math.min(Math.max(fractionDigits, 0), 7) : 0 8 | const num = number.toFixed(frDigits).toString() 9 | 10 | const parts = num.split('.') 11 | let digits = parts[0].split('').reverse() 12 | let sign = '' 13 | if (num < 0) {sign = digits.pop()} 14 | let final = [] 15 | let pos = 0 16 | 17 | while (digits.length > 1) { 18 | final.push(digits.shift()) 19 | pos++ 20 | if (pos % 3 === 0) {final.push(thousandSeperator)} 21 | } 22 | final.push(digits.shift()) 23 | return `${sign}${final.reverse().join('')}${frDigits > 0 ? fractionSeperator : ''}${frDigits > 0 && parts[1] ? parts[1] : ''}` 24 | } 25 | 26 | export function timeToHuman(time, suffix = 'ms', smaller) { 27 | if (time / 1000 >= 0.01 || typeof smaller === "undefined") { 28 | return ({numberFormatter(time / 1000, 2)} 29 | {suffix}); 30 | } 31 | else { 32 | return ({numberFormatter(time, 2)} 33 | {smaller}); 34 | } 35 | } 36 | 37 | export function EntriesInfo({routine, parentEntries}) { 38 | const jitText = (routine.jit_entries * 100 / routine.entries).toPrecision(3) 39 | return ( 40 |
41 | 42 | { 43 | jitText !== "0.00" ? jitText : "0" 44 | }% jit 45 | 46 |
47 | {numberFormatter(routine.entries)} 48 | { 49 | typeof parentEntries !== "undefined" 50 | ?
51 | {numberFormatter(routine.entries / parentEntries, 2)} per entry 52 |
53 | : null 54 | } 55 | ) 56 | } 57 | 58 | export function ExclusiveInclusiveTime({routine, maxTime}) { 59 | var barWidthFirst, barWidthSecond, barWidthRest; 60 | const exclusive = routine.exclusive_time || routine.exclusive; 61 | const inclusive = routine.inclusive_time || routine.inclusive; 62 | let willShowBar = typeof maxTime !== "undefined" && exclusive <= inclusive; 63 | if (typeof maxTime !== "undefined") { 64 | barWidthFirst = (exclusive / maxTime) * 100; 65 | barWidthSecond = ((inclusive - exclusive) / maxTime) * 100; 66 | barWidthRest = (1 - inclusive / maxTime) * 100; 67 | } 68 | const barStyle = { 69 | height: "0.5em", 70 | padding: "0px", 71 | margin: "0px", 72 | display: "inline-block" 73 | } 74 | return ( 75 | 76 | {timeToHuman(exclusive, "ms", "µs")} / {timeToHuman(inclusive, "ms", "µs")}
77 | { 78 | routine.entries > 1 && 79 | 80 | {timeToHuman(exclusive / routine.entries, "ms", "µs")} / {" "} 81 | {timeToHuman(inclusive / routine.entries, "ms", "µs")} 82 | {" "}per entry 83 | 84 | } 85 | { 86 | willShowBar && ( 87 | 88 |
89 |
95 | 99 | 103 | 107 |
108 |
109 | 110 | ) 111 | } 112 | ) 113 | } 114 | 115 | export function allocationInfo({routine}) { 116 | 117 | } 118 | 119 | export function RoutineFileInfo({routine}) { 120 | var link; 121 | if (routine.file.startsWith("CORE::v6")) { 122 | var withoutBeginning = routine.file.replace("CORE::v6", ""); 123 | var coreVersion = withoutBeginning.substring(1, 2); 124 | withoutBeginning = withoutBeginning.substring(4); 125 | link = 126 | { routine.file }:{ routine.line } 127 | } 128 | else if (routine.file.startsWith("CORE::")) { 129 | link = 130 | { routine.file }:{ routine.line } 131 | } 132 | else { 133 | link = {routine.file}:{routine.line} 134 | } 135 | return { link } 136 | } 137 | 138 | export function RoutineNameInfo({routine, searchResults}) { 139 | return ( 140 | { typeof searchResults === "number" && searchResults > 0 &&
} 141 | {routine.name} 142 | 143 |
144 | 145 | ) 146 | } 147 | 148 | export function LinkButton({icon, target, disabled}) { 149 | if (disabled) { 150 | return ( 151 | 154 | 155 | ) 156 | } 157 | return ( 158 | 161 | 162 | ) 163 | } 164 | 165 | export function InlineInfo({routine}) { 166 | const inlineText = (routine.inlined_entries * 100 / routine.entries).toPrecision(3); 167 | return ( 168 | inlineText === "0.00" 169 | ? - 170 | : {inlineText}% inlined 171 | ) 172 | } 173 | -------------------------------------------------------------------------------- /frontend/profiler/components/Routine.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import { Container, Button, Table, Nav, NavItem, NavLink } from 'reactstrap'; 3 | import classnames from 'classnames'; 4 | import $ from 'jquery'; 5 | 6 | import RoutineList from './RoutineList'; 7 | import RoutinePaths from './RoutinePaths'; 8 | 9 | import { 10 | ExclusiveInclusiveTime, 11 | EntriesInfo, 12 | InlineInfo, 13 | RoutineNameInfo, 14 | } from './RoutinePieces'; 15 | import { AllocTableContent } from './AllocationParts'; 16 | import CallersList from './CallersList'; 17 | import { RoutineListHeaderComponent } from './RoutineOverviewPage'; 18 | 19 | /** 20 | * @typedef {{ 21 | routine: *; 22 | metadata: *; 23 | columns: *; 24 | expanded: *; 25 | allRoutineChildren: *; 26 | onExpandButtonClicked: *; 27 | maxTime: *; 28 | parentEntries: *; 29 | }} RoutineProps 30 | */ 31 | 32 | export default class Routine extends Component { 33 | constructor(props) { 34 | super(props); 35 | this.scrollRowRef = createRef(); 36 | this.state = { 37 | tab: 'callees', 38 | allocations: null, 39 | allocsError: null, 40 | }; 41 | } 42 | 43 | componentDidMount() { 44 | if (this.props.shouldScrollTo === this.props.routine.id) { 45 | this.scrollRowRef.scrollIntoView({ behavior: 'smooth' }); 46 | } 47 | } 48 | 49 | componentDidUpdate(prevProps, _prevState) { 50 | if (typeof this.props.shouldScrollTo !== 'undefined') { 51 | if ( 52 | typeof prevProps.shouldScrollTo === 'undefined' || 53 | prevProps.shouldScrollTo !== this.props.shouldScrollTo 54 | ) { 55 | if (this.props.shouldScrollTo === this.props.routine.id) { 56 | this.scrollRowRef.scrollIntoView({ behavior: 'smooth' }); 57 | } 58 | } 59 | } 60 | } 61 | 62 | requestAllocations() { 63 | const stateChangeForAlloc = (self, allocs) => { 64 | self.setState(() => ({ 65 | allocations: allocs, 66 | })); 67 | }; 68 | 69 | $.ajax({ 70 | url: `/routine-allocations/${encodeURIComponent(this.props.routine.id)}`, 71 | type: 'GET', 72 | contentType: 'application/json', 73 | success: allocs => stateChangeForAlloc(this, allocs), 74 | error: (xhr, errorStatus, errorText) => { 75 | this.setState(() => ({ allocsError: errorStatus + errorText })); 76 | }, 77 | }); 78 | } 79 | 80 | render() { 81 | const { 82 | routine, 83 | metadata, 84 | expanded, 85 | allRoutineChildren, 86 | onExpandButtonClicked, 87 | maxTime, 88 | parentEntries, 89 | shouldScrollTo, 90 | } = this.props; 91 | let { columns } = this.props; 92 | 93 | if (routine === null) { 94 | return ''; 95 | } 96 | if (typeof columns === 'string') { 97 | columns = columns.split(' '); 98 | } 99 | const myMetadata = metadata[routine.id]; 100 | const columnFunctions = { 101 | expand() { 102 | return ( 103 | 104 | 107 | 108 | ); 109 | }, 110 | sitecount() { 111 | return {routine.sitecount}; 112 | }, 113 | nameInfo() { 114 | return ; 115 | }, 116 | entriesInfo() { 117 | return ( 118 | 123 | ); 124 | }, 125 | exclusiveInclusiveTime() { 126 | return ( 127 | 132 | ); 133 | }, 134 | inlineInfo() { 135 | return ; 136 | }, 137 | }; 138 | let expandedComponent = ; 139 | if (expanded) { 140 | if (this.state.tab === 'callees') { 141 | expandedComponent = ( 142 | 143 | {(!allRoutineChildren || 144 | typeof allRoutineChildren[routine.id] === 'undefined') && ( 145 | Loading, hold on ... 146 | )} 147 | {allRoutineChildren && allRoutineChildren[routine.id] && ( 148 | 157 | )} 158 | 159 | ); 160 | } else if (this.state.tab === 'callers') { 161 | expandedComponent = ( 162 | 163 | 164 | 165 | ); 166 | } else if (this.state.tab === 'paths') { 167 | expandedComponent = ( 168 | 169 | 170 | 171 | ); 172 | } else if (this.state.tab === 'allocations') { 173 | if (this.state.allocations === null) { 174 | expandedComponent = ( 175 | 176 | Loading, hold on... 177 | 178 | ); 179 | } else { 180 | expandedComponent = ( 181 | 182 | 183 | 184 | 188 | 189 |
190 |
191 | ); 192 | } 193 | } 194 | } 195 | return [ 196 | 201 | {columns.map(name => columnFunctions[name]())} 202 | , 203 | expanded && ( 204 | 205 | 209 | 258 | {expandedComponent} 259 | 260 | 261 | ), 262 | ]; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /frontend/profiler/components/RoutinePaths.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | import React, { Component } from 'react'; 3 | import $ from 'jquery'; 4 | import { Button, Table } from 'reactstrap'; 5 | import { Link } from 'react-router-dom'; 6 | import classnames from 'classnames'; 7 | 8 | /** 9 | * @typedef {{routineId: *;callIdList: *;allRoutines: *;}} RoutinePathsProps 10 | */ 11 | 12 | export default class RoutinePaths extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | isLoading: false, 17 | error: null, 18 | paths: [], 19 | pathDepth: 0, 20 | minimalView: false, 21 | }; 22 | } 23 | 24 | componentDidMount() { 25 | this.state.isLoading = true; 26 | function calculateDepth(tree) { 27 | let childArray; 28 | if (tree instanceof Array) { 29 | childArray = tree; 30 | } else { 31 | if (tree.children.length === 0) { 32 | return 0; 33 | } 34 | childArray = tree.children; 35 | } 36 | console.log('calculating depth of', childArray); 37 | let max = 0; 38 | for (const children of childArray) { 39 | const childVal = calculateDepth(children.children); 40 | if (childVal > max) max = childVal; 41 | } 42 | return max + 1; 43 | } 44 | const url = 45 | typeof this.props.routineId === 'undefined' 46 | ? `/call-paths/${encodeURIComponent(this.props.callIdList.join(','))}` 47 | : `/routine-paths/${encodeURIComponent(this.props.routineId)}`; 48 | $.ajax({ 49 | url, 50 | type: 'GET', 51 | contentType: 'application/json', 52 | success: paths => 53 | this.setState(() => ({ 54 | isLoading: false, 55 | paths, 56 | pathDepth: calculateDepth(paths), 57 | })), 58 | error: (xhr, errorStatus, errorText) => { 59 | this.setState(() => ({ 60 | isLoading: false, 61 | error: errorStatus + errorText, 62 | })); 63 | }, 64 | }); 65 | } 66 | 67 | toggleMinimalView() { 68 | this.setState(state => ({ minimalView: !state.minimalView })); 69 | } 70 | 71 | render() { 72 | if (this.state.isLoading) { 73 | return
Hold on ...
; 74 | } 75 | if (this.state.error) { 76 | return
Error occurred: {this.state.error}
; 77 | } 78 | const result = []; 79 | let row = []; 80 | const showLines = []; 81 | let key = 0; 82 | const self = this; 83 | const tdStyle = { 84 | borderLeft: '1px solid darkgrey', 85 | padding: '0.3em !important', 86 | paddingLeft: '12px !important', 87 | }; 88 | const isHighlighted = 89 | typeof this.props.routineId === 'undefined' 90 | ? entry => 91 | this.props.callIdList.filter(listEntry => entry.call === listEntry).length > 0 92 | : entry => entry.routine === this.props.routineId; 93 | function digestNode(children, node, depth = 1) { 94 | console.log('digesting children:', children, node); 95 | if (children.length > 0) { 96 | let first = 1; 97 | let last = children.length; 98 | for (const child of children) { 99 | const NameDisplay = ({ routine }) => 100 | (self.state.minimalView ? 'X' : self.props.allRoutines[routine].name); 101 | if (children.length > 1 && last-- > 1) { 102 | showLines[depth] = 1; 103 | } else { 104 | showLines[depth] = 0; 105 | } 106 | if (row.length === 0 && depth > 1) { 107 | row = Array(depth - 1) 108 | .fill(0) 109 | .map((x, idx) => ( 110 | 114 | )); 115 | console.log(row); 116 | } 117 | if (node.routine === null) { 118 | row.push( 119 | 1 })} 122 | style={{ ...tdStyle }} 123 | > 124 | {self.state.minimalView ? '»' : 'Entry'} 125 | , 126 | ); 127 | } else { 128 | const childRoutine = self.props.allRoutines[child.routine]; 129 | const linkStyle = self.state.minimalView 130 | ? { display: 'block', backgroundColor: childRoutine.color } 131 | : {}; 132 | if (!self.state.minimalView) { 133 | tdStyle.borderBottom = `4px solid ${childRoutine.color}`; 134 | } 135 | console.log(child); 136 | console.log(isHighlighted(child)); 137 | row.push( 138 | 1, 142 | highlighted: isHighlighted(child), 143 | })} 144 | style={{ ...tdStyle }} 145 | > 146 | 153 | 154 | 155 | ); 156 | if (!self.state.minimalView) { 157 | tdStyle.borderBottom = undefined; 158 | } 159 | } 160 | digestNode(child.children, child, depth + 1); 161 | if (first-- <= 0 && row.length > 0) { 162 | result.push( 163 | 164 | {row} 165 | ); 166 | row = []; 167 | } 168 | } 169 | } else { 170 | /* 171 | if (node) 172 | row.push({self.props.allRoutines[node.routine].name} {node.call}); 173 | else 174 | row.push(What?); 175 | */ 176 | result.push( 177 | 178 | {row} 179 | ); 180 | row = []; 181 | } 182 | } 183 | for (const thread of this.state.paths) { 184 | digestNode(thread.children.children, thread.children); 185 | } 186 | 187 | result.push({row}); 188 | return ( 189 | 190 | 220 | {this.state.minimalView ? ( 221 | 242 | ) : ( 243 | '' 244 | )} 245 | 251 | 252 | {result} 253 |
254 |
255 | ); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /lib/HeapAnalyzerWeb.pm6: -------------------------------------------------------------------------------- 1 | use v6.d.PREVIEW; 2 | use OO::Monitors; 3 | 4 | use App::MoarVM::HeapAnalyzer::Model; 5 | 6 | monitor HeapAnalyzerWeb { 7 | 8 | has App::MoarVM::HeapAnalyzer::Model $.model; 9 | 10 | has Supplier $!status-updates = Supplier::Preserving.new; 11 | 12 | has $!latest-model-state = "nothing"; 13 | 14 | has %.operations-in-progress; 15 | 16 | has $!highscores; 17 | 18 | has Supplier $!progress-updates = Supplier::Preserving.new; 19 | 20 | method load-file($file is copy) { 21 | $file = $file.IO; 22 | die "$file does not exist" unless $file.e; 23 | die "$file does not a file" unless $file.f; 24 | die "$file does not readable" unless $file.r; 25 | note "trying to open a model file"; 26 | my $resolve = Promise.new; 27 | start { 28 | note "started!"; 29 | with $!model { 30 | die "Switching models NYI"; 31 | } 32 | note "sending pre-load message"; 33 | $!latest-model-state = "pre-load"; 34 | $!status-updates.emit({ model_state => "pre-load" }); 35 | note "building the model now"; 36 | $!model .= new(:$file); 37 | note "going to resolve"; 38 | $resolve.keep; 39 | if $!model.num-snapshots == 1 { 40 | self.request-snapshot(0); 41 | } 42 | note "done, yay"; 43 | $!latest-model-state = "post-load"; 44 | $!status-updates.emit({ model_state => "post-load", |self.model-overview }); 45 | $!model.highscores.then({ 46 | my @scores = .result; 47 | my %lookups; 48 | my $whole-result = %(); 49 | for [, ] -> @bits { 50 | my @all-indices; 51 | my @per-position-entries; 52 | for @scores -> $snap-score { 53 | for $snap-score{@bits.head(2)} { 54 | @all-indices.append($_.list); 55 | } 56 | } 57 | @all-indices .= unique; 58 | %lookups{@bits.tail} = @bits.tail eq "frames" 59 | ?? (@all-indices Z=> $!model.resolve-frames(@all-indices)).hash 60 | !! (@all-indices Z=> $!model.resolve-types(@all-indices)).hash; 61 | } 62 | 63 | for [, ] -> @bits { 64 | for @scores -> $snap-score { 65 | state $idx = 0; 66 | for @bits.head(2) -> $bit { 67 | my %right-lookup := %lookups{@bits.tail}; 68 | my &formatter = @bits.tail eq "frames" ?? { (. || "") ~ " $_.:$_" } !! { . }; 69 | for $snap-score{$bit}.list Z $snap-score{$bit}.list { 70 | $whole-result{$bit}[$idx]{formatter %right-lookup{$_[0]}} = $_[1]; 71 | } 72 | } 73 | $idx++; 74 | } 75 | } 76 | 77 | $whole-result = %lookups; 78 | 79 | $!highscores = $whole-result; 80 | 81 | $!status-updates.emit({ model_state => "post-load", |self.model-overview }); 82 | LEAVE note "highscores calculated in $( now - ENTER now )s"; 83 | CATCH { .say } 84 | }); 85 | CATCH { 86 | note "error loading heap snapshot"; 87 | $!status-updates.emit( 88 | %(model_state => "error-load", 89 | error_type => $_.^name, 90 | error_message => $_.message)); 91 | $resolve.break($_); 92 | } 93 | } 94 | note "waiting for resolve to happen"; 95 | await $resolve; 96 | } 97 | 98 | method status-messages() { 99 | LEAVE { 100 | note "status messages opened; model state is $!latest-model-state"; 101 | if $!latest-model-state eq "post-load" { 102 | Promise.in(0.1).then({ $!status-updates.emit({ model_state => "post-load", |self.model-overview }) }); 103 | } 104 | if $!latest-model-state eq "nothing" { 105 | Promise.in(0.1).then({ $!status-updates.emit({ model_state => "nothing" }) }); 106 | } 107 | $!status-updates.Supply.tap({ note "heap status message: $_" }); 108 | } 109 | $!status-updates.Supply 110 | } 111 | 112 | method progress-messages() { 113 | LEAVE { 114 | for %.operations-in-progress { 115 | $!progress-updates.emit($_); 116 | } 117 | } 118 | $!progress-updates.Supply 119 | } 120 | 121 | method model-overview() { 122 | with $!model { 123 | { 124 | num_snapshots => $!model.num-snapshots, 125 | loaded_snapshots => ^$!model.num-snapshots .map({ $%( state => $!model.snapshot-state($_).Str ) }), 126 | |%(summaries => $_ with $!model.summaries), 127 | |%(highscores => $_ with $!highscores), 128 | } 129 | } 130 | else { 131 | %( 132 | model_state => "unprepared", 133 | |(suggested_filename => $_ with %*ENV)) 134 | } 135 | } 136 | 137 | method !make-update-key { 138 | (flat "a".."z", "A".."Z").pick(16).join(""); 139 | } 140 | 141 | method request-snapshot($index) { 142 | note "requested snapshot at ", DateTime.now; 143 | 144 | die "the model needs to load up first" without $!model; 145 | die "Snapshot ID $index out of range (0..$!model.num-snapshots())" 146 | unless $index ~~ 0..^$!model.num-snapshots(); 147 | 148 | die unless $!model.snapshot-state($index) ~~ SnapshotStatus::Unprepared; 149 | 150 | $!status-updates.emit: %( 151 | snapshot_index => $index, 152 | snapshot_state => { state => "Preparing" }); 153 | 154 | my Supplier $updates .= new; 155 | 156 | my $update-key = self!make-update-key; 157 | 158 | $!progress-updates.emit: %( 159 | progress => [0, 1, 0], 160 | description => "reading snapshot $index", 161 | uuid => $update-key, 162 | :!cancellable, 163 | ); 164 | 165 | start react { 166 | whenever $updates.Supply -> $message { 167 | if $message:exists { 168 | $!progress-updates.emit: %( 169 | uuid => $update-key, 170 | progress => $message, 171 | :!cancellable, 172 | ); 173 | } 174 | } 175 | whenever $!model.promise-snapshot($index, :$updates) { 176 | $!status-updates.emit: %( 177 | snapshot_index => $index, 178 | snapshot_state => { state => $!model.snapshot-state($index).Str }, 179 | is_done => True, 180 | ) 181 | } 182 | } 183 | 184 | $update-key; 185 | } 186 | 187 | method collectable-data($snapshot, $index) { 188 | die unless $!model.snapshot-state($snapshot) ~~ SnapshotStatus::Ready; 189 | 190 | with $!model.promise-snapshot($snapshot).result -> $s { 191 | $s.col-details($index); 192 | } 193 | } 194 | 195 | method collectable-outrefs($snapshot, $index) { 196 | die unless $!model.snapshot-state($snapshot) ~~ SnapshotStatus::Ready; 197 | 198 | with $!model.promise-snapshot($snapshot).result -> $s { 199 | my @parts = $s.details($index); 200 | my @pieces; 201 | @pieces.push: @parts.shift; 202 | 203 | my %categories := @parts.rotor(2).classify({ .head, .tail.key }); 204 | 205 | %(do for %categories { 206 | .key => %(do for .value.list -> $typepair { 207 | $typepair.key => do for $typepair.value.list { .[1].value } 208 | }) 209 | }) 210 | } 211 | } 212 | 213 | method collectable-inrefs($snapshot, $index) { 214 | die unless $!model.snapshot-state($snapshot) ~~ SnapshotStatus::Ready; 215 | 216 | my $updates = Supplier::Preserving.new; 217 | my $update-key = self!make-update-key; 218 | 219 | start react whenever $updates.Supply -> $message { 220 | if $message:exists { 221 | $!progress-updates.emit: %( 222 | description => "calculating incoming refs for snapshot $snapshot", 223 | uuid => $update-key, 224 | progress => $message, 225 | :!cancellable, 226 | ); 227 | } 228 | } 229 | 230 | with $!model.promise-snapshot($snapshot).result -> $s { 231 | my $rev-refs = $s.reverse-refs($index, :$updates).squish.cache; 232 | my %categories = $rev-refs.classify(*.value, as => *.key); 233 | 234 | [%categories.sort({ .value.elems, .value.head }).map({ .key, .value })] 235 | } 236 | } 237 | 238 | my constant %kind-map = hash 239 | objects => CollectableKind::Object, 240 | stables => CollectableKind::STable, 241 | frames => CollectableKind::Frame; 242 | 243 | method toplist($kind, $by, $snapshot, $count = 100, $start = 0) { 244 | die "snapshot not loaded" unless $!model.snapshot-state($snapshot) ~~ SnapshotStatus::Ready; 245 | die "invalid kind $kind" unless %kind-map{$kind}:exists; 246 | 247 | with $!model.promise-snapshot($snapshot).result -> $s { 248 | my @tops = $s."top-by-$by"($count + $start, %kind-map{$kind}) 249 | } 250 | } 251 | 252 | method find(Int $snapshot, Str $kind, $condition, $target, Int $count, Int $start) { 253 | die "snapshot not loaded" unless $!model.snapshot-state($snapshot) ~~ SnapshotStatus::Ready; 254 | die "invalid kind $kind" unless %kind-map{$kind}:exists; 255 | 256 | dd $count, $start; 257 | 258 | with $!model.promise-snapshot($snapshot).result -> $s { 259 | my $tops = $s.find($count + $start, %kind-map{$kind}, $condition, $target); 260 | say $tops.values.elems; 261 | $tops.values.map({ %( id => .[0], descr => .[1], size => .[2] ) }); 262 | } 263 | } 264 | 265 | method path(Int $snapshot, Int $collectable) { 266 | die unless $!model.snapshot-state($snapshot) ~~ SnapshotStatus::Ready; 267 | 268 | my $updates = Supplier::Preserving.new; 269 | my $update-key = self!make-update-key; 270 | 271 | start react whenever $updates.Supply -> $message { 272 | if $message:exists { 273 | $!progress-updates.emit: %( 274 | description => "calculating pathing for snapshot $snapshot", 275 | uuid => $update-key, 276 | progress => $message, 277 | :!cancellable, 278 | ); 279 | } 280 | } 281 | 282 | with $!model.promise-snapshot($snapshot).result -> $s { 283 | $s.path($collectable, :$updates).duckmap(-> Pair $p { [$p.key, $p.value] }); 284 | } 285 | } 286 | 287 | method request-shared-data { 288 | %( 289 | types => $!model.resolve-types(^$!model.num-types), 290 | frames => $!model.resolve-frames(^$!model.num-frames), 291 | ) 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /lib/Routes.pm6: -------------------------------------------------------------------------------- 1 | use Cro::HTTP::Router; 2 | use Cro::HTTP::Router::WebSocket; 3 | use JSON::Fast; 4 | use HeapAnalyzerWeb; 5 | use ProfilerWeb; 6 | use CodeRunner; 7 | 8 | sub json-content($route, &code) { 9 | say "json content: $route"; 10 | my $start = now; 11 | my $result = code(); 12 | note " $route in { now - $start }"; 13 | $start = now; 14 | my $json-result = to-json($result, :!pretty); 15 | note " $route json in { now - $start }: $json-result.chars() characters"; 16 | content "application/json", $json-result; 17 | } 18 | 19 | sub routes(HeapAnalyzerWeb $model, ProfilerWeb $profiler, $filename?) is export { 20 | sub load-file(Str $path) { 21 | if $path.ends-with("sql" | "sqlite3") { 22 | note "opening an sql thing"; 23 | $profiler.load-file($path); 24 | note "done"; 25 | return { filetype => "profile" }; 26 | } else { 27 | note "opening a heap thing"; 28 | $model.load-file($path); 29 | note "done"; 30 | return { filetype => "heapsnapshot" }; 31 | } 32 | } 33 | 34 | try { 35 | load-file($_) with $filename; 36 | CATCH { 37 | default { 38 | say "Could not load $filename from disk:"; 39 | say ""; 40 | .message.say; 41 | say ""; 42 | say "Continuing without a loaded file."; 43 | } 44 | } 45 | } 46 | 47 | route { 48 | get -> { 49 | static 'static/index.html' 50 | } 51 | 52 | get -> 'js', *@path { 53 | static 'static/js', @path; 54 | } 55 | 56 | get -> 'js', 'bootstrap.bundle.js' { static 'frontend/node_modules/bootstrap/dist/js/bootstrap.bundle.js' } 57 | get -> 'js', 'bootstrap.bundle.js.map' { static 'frontend/node_modules/bootstrap/dist/js/bootstrap.bundle.js.map' } 58 | 59 | get -> 'css', *@path { 60 | static 'static/css', @path; 61 | } 62 | 63 | get -> 'css', 'bootstrap.css' { static 'frontend/node_modules/bootstrap/dist/css/bootstrap.css' } 64 | get -> 'css', 'bootstrap-grid.css' { static 'frontend/node_modules/bootstrap/dist/css/bootstrap-grid.css' } 65 | get -> 'css', 'bootstrap-reboot.css' { static 'frontend/node_modules/bootstrap/dist/css/bootstrap-reboot.css' } 66 | 67 | get -> 'imagery', *@path { 68 | static 'static/imagery', @path; 69 | } 70 | 71 | get -> 'whats-loaded' { 72 | if $profiler.is-loaded -> $filename { 73 | content 'application/json', { filetype => "profile", filename => $filename } 74 | } 75 | # XXX something for the heap analyzer 76 | elsif $model.model.defined { 77 | content 'application/json', { filetype => "heap", filename => "who knows" } 78 | } 79 | else { 80 | content 'application/json', %( ) 81 | } 82 | } 83 | 84 | post -> 'load-file' { 85 | request-body -> (Str :$path?) { 86 | say $path.perl; 87 | content 'application/json', load-file($path); 88 | } 89 | } 90 | 91 | post -> 'browse' { 92 | request-body -> (Str :$type = "script") { 93 | content "application/json", {filenames => CodeRunner.get-interesting-local-files(:$type)}; 94 | } 95 | } 96 | 97 | get -> 'overview-data' { 98 | json-content "overview-data", { $profiler.overview-data() } 99 | } 100 | 101 | get -> 'thread-data' { 102 | json-content "thread-data", { $profiler.thread-data() } 103 | } 104 | 105 | get -> 'recursive-call-children', Int $call-id { 106 | json-content "recursive-call-children", { $profiler.recursive-children-of-call($call-id) } 107 | } 108 | 109 | get -> 'routine-children', Int $routine-id { 110 | json-content "routine-children", { $profiler.all-children-of-routine($routine-id) }; 111 | } 112 | 113 | get -> 'routine-callers', Int $routine-id { 114 | json-content "routine-callers", { $profiler.callers-of-routine($routine-id) }; 115 | } 116 | 117 | get -> 'routine-overview' { 118 | json-content "routine-overview", { $profiler.routine-overview } 119 | } 120 | 121 | get -> 'all-routines' { 122 | json-content "all-routines", { $profiler.all-routines } 123 | } 124 | 125 | get -> 'routine-paths', Int $routine-id { 126 | json-content "routine-paths", { $profiler.routine-paths($routine-id) }; 127 | } 128 | 129 | get -> 'call-paths', Str $call-id { 130 | json-content "call-path", { $profiler.call-path($call-id) }; 131 | } 132 | 133 | get -> 'call-path', Int $call-id { 134 | json-content "call-path", { $profiler.call-path($call-id) }; 135 | } 136 | 137 | get -> 'call-children', Int $call-id, "search", Str $search { 138 | json-content "call-children", { $profiler.search-call-children($call-id, $search) }; 139 | } 140 | 141 | get -> 'call-children', Int $call-id { 142 | json-content "call-children", { $profiler.children-of-call($call-id) }; 143 | } 144 | 145 | get -> 'gc-overview' { 146 | json-content "gc-overview", { $profiler.gc-summary }; 147 | } 148 | 149 | get -> 'gc-details', Int $sequence-num { 150 | json-content "gc-details", { $profiler.gc-details($sequence-num) }; 151 | } 152 | 153 | get -> 'all-allocations' { 154 | json-content "all-allocs", { $profiler.all-allocs }; 155 | } 156 | 157 | get -> 'call-allocations', Int $call { 158 | json-content "call-allocations", { $profiler.call-allocations($call) } 159 | } 160 | 161 | get -> 'routine-allocations', Int $routine { 162 | json-content "routine-allocations", { $profiler.routine-allocations($routine) } 163 | } 164 | 165 | get -> 'routine-spesh-overview' { 166 | json-content "routine-spesh-overview", { $profiler.routine-spesh-overview() } 167 | } 168 | 169 | get -> 'inclusive-call-allocations', Int $call { 170 | json-content "inclusive-call-allocations", { $profiler.call-allocations-inclusive($call) } 171 | } 172 | 173 | get -> 'allocations-per-type' { 174 | json-content "allocations-per-type", { $profiler.allocations-per-type } 175 | } 176 | 177 | get -> 'allocating-routines-per-type', Int $type { 178 | json-content "allocating-routines-per-type", { $profiler.allocating-routines-per-type($type) } 179 | } 180 | 181 | get -> 'deallocations-for-type', Int $type { 182 | json-content "deallocations-for-type", { $profiler.deallocations-for-type($type) } 183 | } 184 | 185 | get -> 'deallocations-for-sequence', Int $seqnum { 186 | json-content "deallocations-for-sequence", { $profiler.deallocations-for-sequence($seqnum) } 187 | } 188 | 189 | get -> 'model-overview' { 190 | content "application/json", to-json($model.model-overview) 191 | } 192 | 193 | post -> 'request-snapshot' { 194 | request-body -> (Int :$index!) { 195 | json-content "request-snapshot", { %( update_key => $model.request-snapshot($index) ) } 196 | } 197 | } 198 | 199 | get -> 'request-heap-shared-data' { 200 | json-content "request-heap-shared-data", { $model.request-shared-data } 201 | } 202 | 203 | get -> 'toplist', Int $snapshot, Str $kind, Str $by, Int $count = 100, Int $start = 0 { 204 | json-content "toplist", { $model.toplist($kind, $by, $snapshot, $count, $start) } 205 | } 206 | 207 | get -> 'collectable-data', Int $snapshot, Int $index { 208 | json-content "collectable-data", { $model.collectable-data($snapshot, $index) } 209 | } 210 | 211 | get -> 'collectable-outrefs', Int $snapshot, Int $index { 212 | json-content "collectable-outrefs", { $model.collectable-outrefs($snapshot, $index) } 213 | } 214 | get -> 'collectable-inrefs', Int $snapshot, Int $index { 215 | json-content "collectable-inrefs", { $model.collectable-inrefs($snapshot, $index) } 216 | } 217 | 218 | get -> 'find', Int $snapshot, Str $kind, Str $condition, Str $target, Int $count = 500, Int $start = 0 { 219 | json-content "find", { $model.find($snapshot, $kind, $condition, $target, $count, $start) } 220 | } 221 | 222 | get -> 'path', Int $snapshot, Int $collectable { 223 | json-content "path", { $model.path($snapshot, $collectable) } 224 | } 225 | 226 | get -> 'heap-status-messages' { 227 | web-socket -> $incoming { 228 | note "subscription to heap status messages"; 229 | supply { 230 | whenever $model.status-messages -> $message { 231 | note "sending off heap event at $(DateTime.now)"; 232 | emit to-json { 233 | :WS_ACTION, 234 | action => { 235 | type => "HEAP_STATUS_UPDATE", 236 | body => $message 237 | } 238 | }, :sorted-keys 239 | } 240 | whenever $model.progress-messages -> $progress { 241 | emit to-json { 242 | :WS_ACTION, 243 | action => { 244 | type => "HEAP_PROGRESS_UPDATE", 245 | body => $progress 246 | } 247 | }, :sorted-keys 248 | } 249 | } 250 | } 251 | } 252 | 253 | get -> 'profile-status-messages' { 254 | note "subscribing to profiler status messages"; 255 | web-socket -> $incoming { 256 | supply { 257 | whenever $profiler.status-messages -> $message { 258 | note "got a status message"; 259 | emit to-json { 260 | WS_ACTION => True, 261 | action => { 262 | type => "PROFILE_STATUS_UPDATE", 263 | body => $message 264 | } 265 | } 266 | note " success!"; 267 | } 268 | QUIT { 269 | note "status message subscription ended"; 270 | note $_; 271 | } 272 | LAST { 273 | note "oh no, why did the status messages get LAST?"; 274 | } 275 | } 276 | } 277 | } 278 | 279 | get -> 'flamegraph-for', Int $call-id where * >= 0 { 280 | json-content "flamegraph-for", { $profiler.data-for-flamegraph($call-id) } 281 | } 282 | 283 | get -> 'flamegraph-for', Int $call-id, "maxdepth", Int $maxdepth where * > 0 { 284 | json-content "flamegraph-for", { $profiler.data-for-flamegraph($call-id, $maxdepth) } 285 | } 286 | 287 | # get -> 'latest-tips' { 288 | # web-socket -> $incoming { 289 | # supply whenever $tipsy.latest-tips -> $tip { 290 | # emit to-json { 291 | # WS_ACTION => True, 292 | # action => { 293 | # type => 'LATEST_TIP', 294 | # id => $tip.id, 295 | # text => $tip.tip 296 | # } 297 | # } 298 | # } 299 | # } 300 | # } 301 | # 302 | # post -> 'tips', Int $id, 'agree' { 303 | # $tipsy.agree($id); 304 | # response.status = 204; 305 | # CATCH { 306 | # when X::Tipsy::NoSuchId { 307 | # not-found; 308 | # } 309 | # } 310 | # } 311 | # 312 | # post -> 'tips', Int $id, 'disagree' { 313 | # $tipsy.disagree($id); 314 | # response.status = 204; 315 | # CATCH { 316 | # when X::Tipsy::NoSuchId { 317 | # not-found; 318 | # } 319 | # } 320 | # } 321 | # 322 | # get -> 'top-tips' { 323 | # web-socket -> $incoming { 324 | # supply whenever $tipsy.top-tips -> @tips { 325 | # emit to-json { 326 | # WS_ACTION => True, 327 | # action => { 328 | # type => 'UPDATE_TOP_TIPS', 329 | # tips => [@tips.map: -> $tip { 330 | # { 331 | # id => $tip.id, 332 | # text => $tip.tip, 333 | # agreed => $tip.agreed, 334 | # disagreed => $tip.disagreed 335 | # } 336 | # }] 337 | # } 338 | # } 339 | # } 340 | # } 341 | # } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import React, {useReducer} from 'react'; 3 | import {render} from 'react-dom'; 4 | import {applyMiddleware, combineReducers, createStore} from 'redux'; 5 | import {connect, Provider} from 'react-redux'; 6 | import {HashRouter, Link, Redirect, Route, Switch, withRouter} from 'react-router-dom'; 7 | import thunkMiddleware from 'redux-thunk'; 8 | import WSAction from 'redux-websocket-action'; 9 | import Loadable from 'react-loadable'; 10 | import ErrorBoundary from 'react-error-boundary'; 11 | 12 | import {Button, Col, Container, Input, InputGroup, InputGroupAddon, Nav, NavItem, NavLink, Row} from 'reactstrap'; 13 | 14 | import * as HeapAnalyzerActions from './heapanalyzer/actions'; 15 | import type {HeapSnapshotState} from './heapanalyzer/reducer'; 16 | import heapAnalyzerReducer from './heapanalyzer/reducer'; 17 | 18 | import * as ProfilerActions from './profiler/actions'; 19 | import profilerReducer from './profiler/reducer'; 20 | 21 | import * as WelcomeActions from './welcome/actions'; 22 | import welcomeReducer from './welcome/reducer'; 23 | 24 | /* import GreetingsPage from './welcome/GreetingsPage'; 25 | import { getGCOverview, getGCDetails } from "./profiler/actions"; */ 26 | 27 | const RoutineOverviewPage = Loadable({ 28 | loader: () => import(/* webpackChunkName: "routinelist" */ './profiler/components/RoutineOverviewPage'), 29 | loading: () =>
Hold on ...
, 30 | }); 31 | 32 | const HeapSnapshotApp = Loadable({ 33 | loader: () => import(/* webpackChunkName: "heapsnapshotapp" */ './heapanalyzer/components/HeapSnapshotApp'), 34 | loading: () =>
Hold on ...
, 35 | }); 36 | 37 | const GCOverview = Loadable({ 38 | loader: () => import(/* webpackChunkName: "gcoverview" */ './profiler/components/GCOverview'), 39 | loading: () =>
Hold on ...
, 40 | }); 41 | 42 | const CallGraph = Loadable({ 43 | loader: () => import(/* webpackChunkName: "callgraph" */ './profiler/components/CallGraph'), 44 | loading: () =>
Hold on ...
, 45 | }) 46 | 47 | const AllocationViewer = Loadable({ 48 | loader: () => import(/* webpackChunkName: "allocationviewer" */ './profiler/components/AllocationViewer'), 49 | loading: () =>
Hold on ...
, 50 | }) 51 | 52 | const OverviewPage = Loadable({ 53 | loader: () => import(/* webpackChunkName: "overviewPage" */ './profiler/components/OverviewPage'), 54 | loading: () =>
Hold on ...
, 55 | }) 56 | 57 | const SpeshOverview = Loadable({ 58 | loader: () => import(/* webpackChunkName: "speshOverview" */ './profiler/components/SpeshOverview'), 59 | loading: () =>
Hold on ...
, 60 | }) 61 | 62 | type SelectFileProps = { 63 | filePath: string, 64 | onChangeFilePath: (string) => void, 65 | onLoadFile: (string) => void, 66 | } 67 | 68 | const SelectFile = (props : SelectFileProps) => ( 69 |
70 |

Enter a local file path: profiler data in sql format or sqlite3 database file

71 |
{ props.onLoadFile(e); e.preventDefault(); }}> 72 | 73 | props.onChangeFilePath(e.target.value)} 76 | /> 77 | 78 | 79 | 80 | 81 |
82 |

How to obtain a profile

83 |

All you have to do is run your script from the commandline the same way you would otherwise call it, 84 | but also pass these arguments directly to perl6, in other words before the script name or -e part:

85 |

perl6 --profile --profile-filename=/tmp/results.sql myScript.p6

86 |

When you open the resulting .sql file in this tool, a .sqlite3 file of the 87 | same name will be put next to it. If it already exists, a random name will be put between the filename 88 | and the extension, so any changes to the .sqlite3 file you may have made will not be 89 | clobbered. However, this tool will not make changes to the file. It is safe to pass the .sqlite3 90 | filename on the second invocation. 91 |

92 |

You can also invoke this tool's service.p6 with a path as the argument. It will load the 93 | given file and immediately redirect you to the profiler when you open the app.

94 |
95 | ); 96 | 97 | const path = (match, extra) => ((match.url.endsWith("/") ? match.url : match.url + "/") + extra); 98 | 99 | const ProfilerApp = props => { 100 | console.log("profiler app match prop:", props.match); 101 | console.log("profiler app location prop:", props.location); 102 | return ( 103 | 104 | 124 | 125 | 126 | 129 | 130 | ( 131 | )} 137 | /> 138 | 139 | 140 | 144 | 145 | 146 | ( 147 | 148 | 154 | 155 | )}/> 156 | ( 157 | 158 | 163 | 164 | )}/> 165 | 166 | 169 | 170 | ( 171 | 172 | 177 | 178 | )}/> 179 | 180 |
oh no.
181 |
182 |
183 |
184 | ); 185 | }; 186 | 187 | type HeapSnapshotAppProps = { 188 | history: any, 189 | tipText: string, 190 | onChangeFilePath: ?(string) => void, 191 | onLoadFile: ?(string) => void, 192 | modelState: string, 193 | loadedSnapshots: ?Array, 194 | onRequestSnapshot: ?(number) => void, 195 | onSwitchSnapshot: (number) => void, 196 | onRequestModelData: () => void, 197 | }; 198 | 199 | function togglerReducer(state, action) { 200 | return !state; 201 | } 202 | function useToggle(startValue) { 203 | const [currentState, dispatchAction] = useReducer(togglerReducer, false); 204 | return [ 205 | currentState, () => dispatchAction("toggle") 206 | ] 207 | } 208 | 209 | const App = (props : HeapSnapshotAppProps) => { 210 | const [isFullscreen, toggleFullscreen] = useToggle(false); 211 | 212 | return ( 213 | 214 | { 215 | isFullscreen && 228 | } 229 | 230 |

231 | 232 | {" "} 233 | 234 | {" "} MoarVM Performance Tool 235 |

236 | 237 | 238 | 239 | { /* */} 240 | 245 | { 246 | props.welcome.frontendMode === "heapsnapshot" ? () : 247 | props.welcome.frontendMode === "profile" ? () : null 248 | } 249 | 250 | 251 | ( 252 | )}> 261 | 262 | ( 263 | 271 | )}/> 272 | 273 | There is nothing at this URL. Return 274 | 275 | 276 |
277 |
278 | ); 279 | }; 280 | 281 | function mapProps(state) { 282 | return state; 283 | } 284 | function mapDispatch(dispatch) { 285 | return { 286 | onChangeFilePath: text => dispatch(WelcomeActions.changeFilePath(text)), 287 | onLoadFile: () => dispatch(WelcomeActions.requestFile()), 288 | 289 | onRequestSnapshot: index => dispatch(HeapAnalyzerActions.requestSnapshot(index)), 290 | onSwitchSnapshot: index => dispatch(HeapAnalyzerActions.switchSnapshot(index)), 291 | onRequestModelData: () => dispatch(HeapAnalyzerActions.requestModelData()), 292 | 293 | onRoutineExpanded: id => dispatch(ProfilerActions.expandRoutine(id)), 294 | onRequestGCOverview: () => dispatch(ProfilerActions.getGCOverview()), 295 | onRequestRoutineOverview: () => dispatch(ProfilerActions.getRoutineOverview()), 296 | onGCExpandButtonClicked: (seq_nr) => dispatch(ProfilerActions.getGCDetails(seq_nr)), 297 | }; 298 | } 299 | 300 | const appReducer = combineReducers({ 301 | welcome: welcomeReducer, 302 | heapanalyzer: heapAnalyzerReducer, 303 | profiler: profilerReducer, 304 | }); 305 | 306 | const store = createStore(appReducer, applyMiddleware(thunkMiddleware)); 307 | 308 | const { host } = window.location; 309 | 310 | const wsActionHeap = new WSAction(store, `ws://${host}/heap-status-messages`, { 311 | retryCount: 3, 312 | reconnectInterval: 3, 313 | }); 314 | wsActionHeap.start(); 315 | 316 | const wsActionProfile = new WSAction(store, `ws://${host}/profile-status-messages`, { 317 | retryCount: 3, 318 | reconnectInterval: 3, 319 | }); 320 | wsActionProfile.start(); 321 | 322 | $.ajax({ 323 | url: '/whats-loaded', 324 | type: 'GET', 325 | success: body => { 326 | if (body.filetype === "profile") { 327 | if (window.location.hash === "#/") { 328 | window.location.replace("#/prof/home"); 329 | } 330 | } 331 | else if (body.filetype === "heap") { 332 | if (window.location.hash === "#/") { 333 | window.location.replace("#/heap/"); 334 | } 335 | } 336 | } 337 | }); 338 | 339 | const ConnectedApp = withRouter(connect(mapProps, mapDispatch)(App)); 340 | render( 341 | 342 | 343 | 344 | 345 | , 346 | document.getElementById('app'), 347 | ); 348 | -------------------------------------------------------------------------------- /frontend/profiler/components/OverviewPage.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import React from "react"; 3 | 4 | import {Progress, Table} from 'reactstrap'; 5 | 6 | import {Bar, BarChart, XAxis, YAxis} from 'recharts'; 7 | import {LinkButton, numberFormatter, RoutineNameInfo, timeToHuman} from "./RoutinePieces"; 8 | import {AutoSizedFlameGraph, postprocessFlameGraphData} from "./FlameGraph"; 9 | 10 | function FrameCountRow(props: { value: number, entries_total: number, frametypename: string, color: string }) { 11 | return 12 | 13 | {props.frametypename} Frames 14 | 15 | 16 | 17 | 18 | 19 | {numberFormatter(100 * props.value / props.entries_total, 2)}% ({numberFormatter(props.value)}) 20 | 21 | ; 22 | } 23 | 24 | function FlameGraphTitleBar({ allRoutines, call }) { 25 | if (call !== null) { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | ) 36 | } 37 | else { 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
Click a node to select & zoom
47 | ) 48 | } 49 | } 50 | 51 | export default class OverviewPage extends React.Component { 52 | constructor(props) { 53 | super(props); 54 | this.state = { 55 | isLoading: { 56 | overviewData: false, 57 | flameGraph: false, 58 | callInfo: false, 59 | }, 60 | overviewData: null, 61 | flameGraph: null, 62 | flameGraphDepth: null, 63 | flameGraphNodeSelected: null, 64 | flameGraphCallInfo: null, 65 | error: null, 66 | reactToRoutineOverview: null, 67 | } 68 | } 69 | 70 | requestOverviewData() { 71 | console.log("requesting the overview data"); 72 | this.setState((state) => ({ 73 | isLoading: { 74 | ...state.isLoading, 75 | overviewData: true, 76 | flameGraph: true, 77 | } 78 | })); 79 | 80 | const stateChangeForOverviewData = (self, {threads, gcstats, allgcstats, callframestats, allocationstats}) => { 81 | var totalTime = 0; 82 | const timingSpans = threads.map((thread) => { 83 | if (thread.first_entry_time + thread.total_time > totalTime) 84 | totalTime = thread.first_entry_time + thread.total_time; 85 | return ({ 86 | range: [ 87 | thread.first_entry_time / 1000, 88 | (thread.first_entry_time + thread.total_time) / 1000 89 | ], 90 | first_entry_time: thread.first_entry_time / 1000, 91 | thread: thread.thread_id, 92 | }); 93 | }); 94 | self.setState((state) => ( 95 | { 96 | isLoading: {...state.isLoading, overviewData: false}, 97 | overviewData: {threads, gcstats, timingSpans, allgcstats, totalTime, callframestats, allocationstats} 98 | })); 99 | }; 100 | 101 | $.ajax({ 102 | url: '/overview-data', 103 | type: 'GET', 104 | contentType: 'application/json', 105 | success: (overview) => stateChangeForOverviewData(this, overview), 106 | error: (xhr, errorStatus, errorText) => { 107 | this.setState(state => ( 108 | { 109 | isLoading: {...state.isLoading, overviewData: false}, 110 | error: errorStatus + errorText 111 | })) 112 | } 113 | }); 114 | 115 | const stateChangeForFlameGraph = (self, data) => { 116 | let { 117 | node : flamegraph, 118 | incomplete 119 | } = data; 120 | console.log("data:", data); 121 | console.log(flamegraph, incomplete); 122 | const flameGraphData = postprocessFlameGraphData(flamegraph, self.props.allRoutines); 123 | if (self.props.allRoutines.length === 0) { 124 | this.props.onRequestRoutineOverview(); 125 | this.state.reactToRoutineOverview = stateChangeForFlameGraph.bind(this, self, data); 126 | } else { 127 | self.setState((state) => ( 128 | { 129 | isLoading: {...state.isLoading, flameGraph: false}, 130 | flameGraph: flameGraphData.flamegraph, 131 | flameGraphDepth: flameGraphData.maxDepth, 132 | reactToRoutineOverview: null, 133 | })); 134 | } 135 | }; 136 | $.ajax({ 137 | url: '/flamegraph-for/0', 138 | type: 'GET', 139 | contentType: 'application/json', 140 | success: (flamegraph) => stateChangeForFlameGraph(this, flamegraph), 141 | error: (xhr, errorStatus, errorText) => { 142 | this.setState(state => ( 143 | { 144 | isLoading: {...state.isLoading, flameGraph: false}, 145 | error: errorStatus + errorText 146 | })) 147 | } 148 | }); 149 | } 150 | 151 | componentDidMount() { 152 | this.requestOverviewData(); 153 | } 154 | 155 | componentDidUpdate(prevProps, prevState) { 156 | if (this.props.allRoutines.length !== prevProps.allRoutines.length) { 157 | if (prevState.reactToRoutineOverview !== null) { 158 | prevState.reactToRoutineOverview() 159 | } 160 | } 161 | } 162 | 163 | requestCallInfo(callId : integer) { 164 | this.setState(state => ({ 165 | isLoading: { ...state.isLoading, callInfo: true }, 166 | flameGraphNodeSelected: callId, 167 | flameGraphCallInfo: null, 168 | })); 169 | 170 | const stateChangeForChildren = (self, children, currentCallId) => { 171 | if (currentCallId !== self.state.flameGraphNodeSelected) 172 | return; 173 | const childs = Array.from(children); 174 | const thisCall = childs.shift(); 175 | self.setState((state) => ({ 176 | isLoading: { ...state.isLoading, callInfo: false }, 177 | flameGraphCallInfo: thisCall 178 | })); 179 | } 180 | 181 | $.ajax({ 182 | url: '/call-children/' + encodeURIComponent(callId), 183 | type: 'GET', 184 | contentType: 'application/json', 185 | success: (children) => stateChangeForChildren(this, children, callId), 186 | error: (xhr, errorStatus, errorText) => {this.setState(state => ({isLoading: { ...state.isLoading, callId: false }, error: errorStatus + errorText}))} 187 | }); 188 | } 189 | 190 | reactFlamegraphSelection(node, uid) { 191 | const ttip = node.tooltip; 192 | const cidstr = ttip.split(": ")[0]; 193 | const callId = parseInt(cidstr); 194 | this.requestCallInfo(callId); 195 | } 196 | 197 | render() { 198 | console.log("rendering the overview page"); 199 | if (this.state.overviewData === null || this.state.isLoading.overviewData === true) { 200 | return ( 201 |
Hold on...
202 | ) 203 | } 204 | if (this.state.error !== null) { 205 | return ( 206 |
Error: {this.state.error}
207 | ) 208 | } 209 | 210 | const gcstats = this.state.overviewData.gcstats; 211 | const fullGCs = gcstats.filter(item => item.full).length; 212 | const anyGCs = gcstats.length > 0; 213 | 214 | const allgcstats = this.state.overviewData.allgcstats; 215 | 216 | const callframestats = this.state.overviewData.callframestats; 217 | const allocationstats = this.state.overviewData.allocationstats; 218 | 219 | const gcpercent = 100 * allgcstats.total / this.state.overviewData.totalTime; 220 | 221 | const speshcount = callframestats.spesh_entries_total; 222 | const jitcount = callframestats.jit_entries_total; 223 | const interpcount = callframestats.entries_total - (jitcount + speshcount); 224 | 225 | let flamegraph_fragment = ; 226 | 227 | if (this.state.flameGraph !== null && typeof this.state.flameGraph !== "undefined") { 228 | const infoFragment = this.state.flameGraphCallInfo === null && this.state.flamegraph === null 229 | ?
Loading...
230 | : ; 231 | flamegraph_fragment = 232 | 233 | {infoFragment} 234 | this.reactFlamegraphSelection(node, uid)}/> 235 | ; 236 | } 237 | else if (this.state.isLoading.flameGraph) { 238 | flamegraph_fragment =
Loading flame graph...
239 | } 240 | 241 | return ( 242 |
243 |
244 |

Start times of threads

245 | 246 | 247 | 248 | 249 | 250 |
251 |
252 |

Threads

253 |

254 | The profiled code ran for { timeToHuman(this.state.overviewData.totalTime) } 255 |

256 |

257 | At the end of the program, {this.state.overviewData.threads.length} threads were active. 258 |

259 |

260 | The dynamic optimizer ("spesh") has been active for { timeToHuman(this.state.overviewData.threads[0].spesh_time) }. 261 |

262 |
263 |
264 |

GC Performance

265 |

GC runs have taken {numberFormatter(gcpercent, 2)}% of total run time

266 | 267 | 268 | 269 | 270 | 271 |

The Garbage Collector ran {gcstats.length} times. 272 | { 273 | fullGCs && [" ", 274 | {numberFormatter(fullGCs)}, " GC runs inspected the entire heap."] 275 | || " There was never a need to go through the entire heap." 276 | }

277 | 278 | { 279 | anyGCs && (

Minor GC runs took between {" "} 280 | {timeToHuman(allgcstats.min_minor_time)} {" "} 281 | and {" "} 282 | {timeToHuman(allgcstats.max_minor_time)} {" "} 283 | with an average of {" "} 284 | {timeToHuman(allgcstats.avg_minor_time)}

) 285 | } 286 | 287 | { 288 | fullGCs > 1 && (

Major GC runs took between {" "} 289 | {timeToHuman(allgcstats.min_major_time)} {" "} 290 | and {" "} 291 | {timeToHuman(allgcstats.max_major_time)} {" "} 292 | with an average of {" "} 293 | {timeToHuman(allgcstats.avg_major_time)}

) 294 | } 295 | 296 | { 297 | fullGCs === 1 && ( 298 |

The Major GC run took {timeToHuman(allgcstats.max_major_time)}

) 299 | } 300 | 301 | { 302 | anyGCs && ( 303 |

Total time spent in GC was {timeToHuman(allgcstats.total)}

304 | ) 305 | } 306 | { 307 | fullGCs > 0 && ( 308 |

Of that, minor collections accounted 309 | for {timeToHuman(allgcstats.total_minor)} and major collections 310 | accounted for {timeToHuman(allgcstats.total_major)}

311 | ) 312 | } 313 |
314 |
315 |

Call Frames

316 |

In total, {numberFormatter(callframestats.entries_total)} call frames were 317 | entered and exited by the profiled code.

318 |

Inlining eliminated the need to 319 | allocate {numberFormatter(callframestats.entries_total - callframestats.inlined_entries_total)} call 320 | frames (that's {numberFormatter(100 - 100 * callframestats.inlined_entries_total / callframestats.entries_total, 2)}%). 321 |

322 | 323 | 324 | 325 | 326 |
327 |
328 |
329 |

Dynamic Optimization

330 |

331 | Of {numberFormatter(speshcount + jitcount)} specialized or JIT-compiled frames, 332 | there were {numberFormatter(callframestats.deopt_one_total)} deoptimizations 333 | (that's {numberFormatter(100 * callframestats.deopt_one_total / (speshcount + jitcount), 2)}% of all optimized frames). 334 |

335 |

336 | There were {numberFormatter(allocationstats.allocated)} object allocations. The dynamic optimizer was 337 | { 338 | allocationstats.replaced ? 339 | <>{" "} able to eliminate the need to allocate { numberFormatter(allocationstats.replaced) } additional objects {" "} 340 | (that's {numberFormatter(100 * allocationstats.replaced / (allocationstats.replaced + allocationstats.allocated), 2)}%) 341 | : <>{" "}not able to eliminate any allocations through scalar replacement 342 | } 343 |

344 |

345 | There were { numberFormatter(callframestats.deopt_all_total) } global deoptimizations triggered by the profiled code. 346 |

347 |

348 | During profilation, code in hot loops was on-stack-replaced (OSR'd) { numberFormatter(callframestats.osr_total) } times. 349 |

350 |
351 | { flamegraph_fragment } 352 |
353 | ); 354 | } 355 | } -------------------------------------------------------------------------------- /frontend/profiler/components/AllocationViewer.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, useState} from 'react'; 2 | import {Container, Row, Table, Button} from 'reactstrap'; 3 | import $ from 'jquery'; 4 | import { ResponsiveContainer, BarChart, Bar, Tooltip, XAxis, YAxis } from 'recharts'; 5 | 6 | import {EntriesInfo, LinkButton, numberFormatter, RoutineNameInfo} from './RoutinePieces'; 7 | import {AllocNameAndRepr} from "./AllocationParts"; 8 | import RoutinePaths from "./RoutinePaths"; 9 | 10 | const memoize = a => a; 11 | 12 | export function Bytes ({ size, totalCount, extraData, kilo }) { 13 | if (kilo) { 14 | size = size / 1024; 15 | } 16 | return 17 | {numberFormatter(size)} {kilo && "kilo"}bytes {extraData && + x || null} 18 | { 19 | totalCount > 0 && 20 | 21 |
22 | 23 | {numberFormatter(size * totalCount)} {kilo && "kilo"}bytes total 24 | 25 |
26 | || null 27 | } 28 |
29 | } 30 | 31 | function AllocatingRoutineRow(props: { routine: T, allRoutines: any, metadata: any }) { 32 | const [isExpanded, setIsExpanded] = useState(false); 33 | 34 | console.log(props); 35 | 36 | console.log(props.routine.callsites); 37 | 38 | const expandedParts = 39 | !isExpanded 40 | ? 41 | : 42 | 43 | 44 | 45 | return 46 | 47 | 50 | 51 | {props.routine.sitecount} 52 | 53 | 54 | 55 | 57 | 58 | {numberFormatter(props.routine.allocs)} 59 | {numberFormatter(props.routine.replaced)} 60 | 61 | { expandedParts } 62 | 63 | } 64 | 65 | export function AllocRoutineList(props) { 66 | // const HeaderComponent = this.props.HeaderComponent; 67 | 68 | const routines = props.routines; 69 | const metadata = props.metadata; 70 | 71 | return ( 72 |
73 | { /* */ } 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | { 88 | routines.map((routine) => ( 89 | 90 | )) 91 | } 92 | 93 |
SitesNameEntriesSizeCountOptimized Out
94 |
95 | ); 96 | } 97 | 98 | const ignoreNulls = memoize(input => input.filter(a => a !== null)); 99 | 100 | const trimNulls = memoize(input => { 101 | let soakedUp = []; 102 | let output = []; 103 | for (let key in input) { 104 | let value = input[key]; 105 | if (value === null) { 106 | if (output.length != 0) 107 | soakedUp.push({ sequence: key, fresh: 0, seen: 0, gen2: 0 }) 108 | } 109 | else { 110 | output.push(...soakedUp); 111 | output.push(value); 112 | soakedUp = []; 113 | } 114 | } 115 | return output; 116 | }); 117 | 118 | const sumUp = memoize(input => input.map(entry => ({ xAxis: entry.sequence, fresh: entry.fresh, seen: entry.seen, gen2: entry.gen2 }))); 119 | 120 | export class AllocationType extends Component<{ onClick: () => any, alloc: any }> { 121 | constructor(props) { 122 | super(props); 123 | this.state = { 124 | isLoading: { 125 | allocatingRoutines: false, 126 | deallocationHistory: false, 127 | }, 128 | loadErrors: { 129 | allocatingRoutines: null, 130 | deallocationHistory: null, 131 | }, 132 | isExpanded: false, 133 | allocatingRoutines: null, 134 | deallocationHistory: null, 135 | } 136 | } 137 | 138 | handleExpandClick = () => { 139 | console.log("handle expand called"); 140 | if (this.state.isExpanded) { 141 | this.setState((state) => ({ 142 | isExpanded: !state.isExpanded, 143 | isHistoryExpanded: false, 144 | })); 145 | } 146 | else if (!this.state.isLoading.allocatingRoutines && (this.state.allocatingRoutines === null || this.state.loadErrors.allocatingRoutines !== null)) { 147 | this.setState((state) => ({ 148 | isLoading: { ...state.isLoading, allocatingRoutines: true }, 149 | loadErrors: { ...state.loadErrors, allocatingRoutines: null }, 150 | isExpanded: true, 151 | })); 152 | 153 | const stateChangeForRoutines = (it, result, alloc) => { 154 | console.log("changing state for received results"); 155 | it.setState((state) => ({ 156 | isLoading: { ...state.isLoading, allocatingRoutines: false }, 157 | loadErrors: { ...state.loadErrors, allocatingRoutines: null }, 158 | allocatingRoutines: result.map((routine) => ( 159 | { 160 | ...routine, 161 | alloc, 162 | callsites: routine.callsites.split(",").map((a) => parseInt(a)) 163 | })), 164 | })) 165 | }; 166 | 167 | $.ajax({ 168 | url: '/allocating-routines-per-type/' + encodeURIComponent(this.props.alloc.id), 169 | type: 'GET', 170 | contentType: 'application/json', 171 | success: (routines) => stateChangeForRoutines(this, routines, this.props.alloc), 172 | error: (xhr, errorStatus, errorText) => { 173 | this.setState(state => ({ 174 | isLoading: { ...this.state.isLoading, allocatingRoutines: false }, 175 | loadErrors: { 176 | ...state.loadErrors, 177 | allocatingRoutines: 178 | state.loadErrors.allocatingRoutines 179 | + errorStatus 180 | + errorText} 181 | }))} 182 | }) 183 | 184 | this.setState((state) => ({isLoading: {...state.isLoading, allocatingRoutines: true}})); 185 | } 186 | else if (!this.state.isExpanded) { 187 | this.setState((state) => ({ 188 | isExpanded: true 189 | })) 190 | } 191 | } 192 | 193 | handleExpandHistoryClick = () => { 194 | console.log("handle expand history called"); 195 | if (this.state.isHistoryExpanded) { 196 | this.setState((state) => ({ 197 | isHistoryExpanded: !state.isHistoryExpanded, 198 | isExpanded: false, 199 | })); 200 | } 201 | else if (!this.state.isLoading.deallocationHistory && (this.state.deallocationHistory === null || this.state.loadErrors.deallocationHistory !== null)) { 202 | this.setState((state) => ({ 203 | isLoading: { ...state.isLoading, deallocationHistory: true }, 204 | loadErrors: { ...state.loadErrors, deallocationHistory: null }, 205 | isExpanded: false, 206 | isHistoryExpanded: true, 207 | })); 208 | 209 | const stateChangeForDeallocations = (it, result, alloc) => { 210 | console.log("changing state for received results"); 211 | it.setState((state) => ({ 212 | isLoading: { ...state.isLoading, deallocationHistory: false }, 213 | loadErrors: { ...state.loadErrors, deallocationHistory: null }, 214 | deallocationHistory: result 215 | })) 216 | }; 217 | 218 | $.ajax({ 219 | url: '/deallocations-for-type/' + encodeURIComponent(this.props.alloc.id), 220 | type: 'GET', 221 | contentType: 'application/json', 222 | success: (routines) => stateChangeForDeallocations(this, routines, this.props.alloc), 223 | error: (xhr, errorStatus, errorText) => { 224 | this.setState(state => ({ 225 | isLoading: { ...state.isLoading, allocatingRoutines: false }, 226 | loadErrors: { 227 | ...state.loadErrors, 228 | allocatingRoutines: 229 | state.loadErrors.allocatingRoutines 230 | + errorStatus 231 | + errorText} 232 | }))} 233 | }) 234 | 235 | this.setState((state) => ({isLoading: {...state.isLoading, allocatingRoutines: true}})); 236 | } 237 | else if (!this.state.isHistoryExpanded) { 238 | this.setState((state) => ({ 239 | isHistoryExpanded: true 240 | })) 241 | } 242 | } 243 | 244 | render() { 245 | let { 246 | isExpanded, 247 | isHistoryExpanded, 248 | isLoading, 249 | loadErrors, 250 | allocatingRoutines, 251 | deallocationHistory, 252 | } = this.state; 253 | 254 | var expandComponent = ; 255 | 256 | if (isExpanded) { 257 | var expandContent = null; 258 | if (loadErrors.allocatingRoutines === null) { 259 | if (isLoading.allocatingRoutines) { 260 | expandContent =
Loading, please wait...
261 | } 262 | else { 263 | expandContent = 264 |

Routines allocating { this.props.alloc.name }

} 269 | 270 | // columns={"expand sitecount nameInfo "} 271 | /> 272 |
273 | } 274 | } 275 | if (expandContent !== null) 276 | expandComponent = 277 | 278 | 279 | 280 | { expandContent } 281 | 282 | 283 | ; 284 | } 285 | else if (isHistoryExpanded) { 286 | var expandContent = null; 287 | if (loadErrors.deallocationHistory === null) { 288 | if (isLoading.deallocationHistory) { 289 | expandContent =
Loading, please wait...
290 | } 291 | else { 292 | expandContent = 293 |

{this.props.alloc.name} freed in each GC run

294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 |
304 | } 305 | } 306 | if (expandContent !== null) 307 | expandComponent = 308 | 309 | 310 | 311 | { expandContent } 312 | 313 | 314 | ; 315 | 316 | } 317 | 318 | console.log(this.props.alloc); 319 | 320 | return ( 321 | 322 | 323 | 324 | {" "} 327 | {" "} 330 | 333 | 334 | 335 | 336 | 338 | 339 | {numberFormatter(this.props.alloc.count)} 340 | {numberFormatter(this.props.alloc.replaced)} 341 | 342 | { 343 | expandComponent 344 | } 345 | ); 346 | } 347 | } 348 | 349 | export default class AllocationTypeList extends Component { 350 | static defaultProps = { 351 | typeId: null 352 | }; 353 | 354 | constructor(props) { 355 | super(props); 356 | this.state = { 357 | isLoading: { 358 | allAllocations: false, 359 | }, 360 | loadError: { 361 | allAllocations: null, 362 | }, 363 | allAllocations: [], 364 | } 365 | } 366 | 367 | requestAllAllocations() { 368 | this.setState((state) => ({ 369 | isLoading: { 370 | allAllocations: true 371 | } 372 | })); 373 | 374 | const stateChangeForAllAllocs = (self, allocs) => { 375 | self.setState((state) => ({ 376 | allAllocations: allocs, 377 | isLoading: { allAllocations: false }, 378 | loadError: { allAllocations: null }, 379 | })) 380 | }; 381 | 382 | $.ajax({ 383 | url: '/all-allocations', 384 | type: 'GET', 385 | contentType: 'application/json', 386 | success: (allocs) => stateChangeForAllAllocs(this, allocs), 387 | error: (xhr, errorStatus, errorText) => { 388 | this.setState(state => ({ 389 | isLoading: { incAllocs: false }, 390 | loadError: { 391 | allAllocations: 392 | state.loadError.allAllocations 393 | + errorStatus 394 | + errorText} 395 | }))} 396 | }) 397 | } 398 | 399 | componentDidMount() { 400 | this.requestAllAllocations(); 401 | } 402 | 403 | componentDidUpdate(prevProps) { 404 | if (prevProps.id !== this.props.callId) { 405 | this.requestAllAllocations(); 406 | } 407 | } 408 | 409 | render() { 410 | let { 411 | isLoading: { 412 | allAllocations: allAllocationsLoading, 413 | }, 414 | loadError: { 415 | allAllocations: allAllocationsError, 416 | }, 417 | allAllocations 418 | } = this.state; 419 | 420 | if (allAllocationsLoading) { 421 | return (
Hold on...
) 422 | } 423 | 424 | if (allAllocationsError !== null) { 425 | return ( 426 |
427 | Error: { allAllocationsError } 428 |
429 |
) 430 | } 431 | 432 | return ( 433 | 434 | 435 |

All types allocated by this program

436 |
437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | { 450 | allAllocations.map(alloc => 451 | 455 | ) 456 | } 457 | 458 |
Name
REPR
Type PropertiesCountOptimized Out
459 |
460 |
461 | ); 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /frontend/profiler/components/GCOverview.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { ResponsiveContainer, BarChart, Bar, Tooltip, XAxis, YAxis } from 'recharts'; 3 | //import memoize from 'memoize-state'; 4 | import { 5 | ButtonGroup, Button, Container, Row, Col, Table 6 | } from 'reactstrap'; 7 | import ErrorBoundary from 'react-error-boundary'; 8 | import $ from 'jquery'; 9 | 10 | import { timeToHuman, numberFormatter } from './RoutinePieces'; 11 | import {Bytes} from "./AllocationViewer"; 12 | 13 | const memoize = a => a; 14 | 15 | export function sizeToHuman(time) { 16 | return ({numberFormatter(time / 1024, 2)}kB); 17 | } 18 | 19 | const only_major = memoize(stats_per_sequence => stats_per_sequence.filter(entry => entry && entry.full === 1)); 20 | const only_minor = memoize(stats_per_sequence => stats_per_sequence.filter(entry => entry && entry.full === 0)); 21 | const time_diffs = memoize(stats_per_sequence => { 22 | let result = []; 23 | let previous = {latest_end_time: 0}; 24 | for (const entry of stats_per_sequence) { 25 | if (entry == null) { 26 | continue; 27 | } 28 | if (previous == null) { 29 | previous = entry; 30 | } 31 | else { 32 | result.push( 33 | { 34 | sequence_num: entry.sequence_num, 35 | time_since_prev: entry.earliest_start_time - previous.latest_end_time, 36 | earliest_start_time: entry.earliest_start_time, 37 | max_time: entry.max_time, 38 | }); 39 | previous = entry; 40 | } 41 | } 42 | return result; 43 | }); 44 | const ignoreNulls = memoize(input => input.filter(a => a !== null)); 45 | const minimumStartTime = memoize(input => input.map(data => data.start_time).reduce((acc, cur) => acc < cur ? acc : cur, Infinity)); 46 | const makeSpans = input => { 47 | const minStartTime = minimumStartTime(input); 48 | console.log(input); 49 | return input.map( 50 | a => { 51 | console.log(a); 52 | return ( 53 | { 54 | sequence_num: a.sequence_num, 55 | thread_id: a.thread_id, 56 | range: [(a.start_time - minStartTime) / 1000, 57 | (a.start_time + a.time - minStartTime) / 1000], 58 | xAxis: a.thread_id + (a.responsible ? " *" : "") 59 | }); 60 | }); 61 | }; 62 | const relativize = memoize(input => input.map(line => { 63 | const total = line.retained_bytes + line.promoted_bytes + line.cleared_bytes; 64 | return { 65 | ...line, 66 | rel_retained_bytes: line.retained_bytes / total, 67 | rel_promoted_bytes: line.promoted_bytes / total, 68 | rel_cleared_bytes: line.cleared_bytes / total, 69 | } 70 | })); 71 | 72 | const GcTableRow = ({ data, expanded, seq_details, prevData, onGCExpandButtonClicked }) => { 73 | const [isLoading, setIsLoading] = useState(false); 74 | const [hadError, setHadError] = useState(null); 75 | const [typeStats, setTypeStats] = useState(null); 76 | 77 | const isExpanded = expanded[data.sequence_num]; 78 | 79 | useEffect(() => { 80 | if (isExpanded && !isLoading && typeStats === null) { 81 | 82 | const stateChangeForTypes = (it, result) => { 83 | setIsLoading(false); 84 | setHadError(null); 85 | 86 | setTypeStats(result); 87 | }; 88 | 89 | $.ajax({ 90 | url: '/deallocations-for-sequence/' + encodeURIComponent(data.sequence_num), 91 | type: 'GET', 92 | contentType: 'application/json', 93 | success: (types) => stateChangeForTypes(this, types), 94 | error: (xhr, errorStatus, errorText) => { 95 | setIsLoading(false); 96 | setHadError(errorStatus + " " + errorText); 97 | } 98 | }); 99 | 100 | setIsLoading(true); 101 | } 102 | else if (isExpanded && isLoading && typeStats !== null) { 103 | setIsLoading(false); 104 | } 105 | }, [data.sequence_num, expanded[data.sequence_num]]); 106 | 107 | return ( 108 | 109 | 110 | 111 | {data.sequence_num} {data.full ? 112 | : ""} 113 | { 114 | /*data.isFirst === 0 && data.sequence_num !== 0 115 | ? Why? 116 | : ""*/ 117 | } 118 | 119 | {data.participants} 120 | {timeToHuman(data.max_time, "ms spent")} 121 | {timeToHuman(data.earliest_start_time)} 122 | { 123 | typeof prevData === "undefined" 124 | ? null 125 | : {" "} 126 | {timeToHuman(data.earliest_start_time - prevData.earliest_start_time - prevData.max_time)} after prev 127 | 128 | } 129 | 130 | 131 | { 132 | expanded[data.sequence_num] && seq_details && seq_details[data.sequence_num] ? 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | { 157 | ignoreNulls(seq_details[data.sequence_num]).map(data => 158 | 159 | 162 | 165 | 168 | 169 | ) 170 | } 171 | 172 |
ThreadAmountsInter-Gen Roots
160 | { data.thread_id } 161 | 163 | { sizeToHuman(data.retained_bytes) } { sizeToHuman(data.promoted_bytes) } { sizeToHuman(data.cleared_bytes) } 164 | 166 | { data.gen2_roots } 167 |
173 | 174 |
175 |
176 | 177 | 178 | : null 179 | } 180 | { 181 | expanded[data.sequence_num] && typeStats !== null && !hadError ? 182 | <> 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | { 197 | typeStats.map((row, idx) => 198 | 199 | 200 | 201 | 202 | ) 203 | } 204 | 205 |
TypeFreed very earlyFreed earlyFreed late
{ row.type_name }{ numberFormatter(row.fresh) }{ numberFormatter(row.seen) }{ numberFormatter(row.gen2) }
206 | 207 | 208 | 209 | : null 210 | } 211 |
212 | ); 213 | } 214 | 215 | const GcTable = ({ overview, expanded, seq_details, onGCExpandButtonClicked }) => { 216 | if (typeof overview === "undefined" || typeof overview.stats_per_sequence === "undefined") 217 | return ( 218 | nothing 219 | ); 220 | if (overview.stats_per_sequence.length === 0) { 221 | return (There were no GC runs during the recording.); 222 | } 223 | let seen = -1; 224 | const rowInputData = ignoreNulls(overview.stats_per_sequence); 225 | return rowInputData.map((data, index) => 226 | 0 ? rowInputData[index - 1] : undefined} expanded={expanded} seq_details={seq_details} onGCExpandButtonClicked={onGCExpandButtonClicked} /> 227 | ) 228 | } 229 | 230 | export default function GCOverview(props) { 231 | const totalTime = typeof props.overview.stats_per_sequence === "undefined" 232 | ? 0 233 | : Array.from(props.overview.stats_per_sequence).filter(d => d !== null).map(d => d.max_time).reduce((a, b) => a + b, 0); 234 | // 0 == hide major, 1 == show all, 2 == only major 235 | const [filterMode, setFilterMode] = useState(1); 236 | const [isLoading, setIsLoading] = useState(false); 237 | 238 | // 0 == split bars, 1 == stacked bars absolute, 2 == stacked bars relative (every row is 100% tall) 239 | const [stackedBarMode, setStackedBarMode] = useState(2); 240 | 241 | useEffect(() => { 242 | if (!isLoading && typeof props.overview === "undefined" || typeof props.overview.stats_per_sequence === "undefined") { 243 | props.onRequestGCOverview(); 244 | setIsLoading(true); 245 | } 246 | else if (isLoading && !(typeof props.overview === "undefined" || typeof props.overview.stats_per_sequence === "undefined")) { 247 | setIsLoading(false); 248 | } 249 | }, [props.overview, props.overview.stats_per_sequence]); 250 | 251 | if (isLoading) { 252 | return ( 253 | 254 | 255 | Loading, please wait ... 256 | 257 | 258 | ) 259 | } 260 | if (typeof props.overview === "undefined" || typeof props.overview.stats_per_sequence === "undefined") { 261 | return ( 262 | 263 | 264 | 265 | 266 | 267 | ) 268 | } 269 | const sourceOfData = relativize(ignoreNulls(props.overview.stats_per_sequence)); 270 | const dataToUse = filterMode === 0 271 | ? only_minor(sourceOfData) 272 | : filterMode === 2 273 | ? only_major(sourceOfData) 274 | : sourceOfData; 275 | 276 | 277 | 278 | const colorForDataKey = { 279 | promoted_bytes: "#f32", 280 | retained_bytes: "#fa5", 281 | cleared_bytes: "#3f3", 282 | 283 | rel_promoted_bytes: "#f32", 284 | rel_retained_bytes: "#fa5", 285 | rel_cleared_bytes: "#3f3", 286 | }; 287 | 288 | const tooltipTextForDataKey = { 289 | promoted_bytes: "Promoted", 290 | retained_bytes: "Retained", 291 | cleared_bytes: "Cleared", 292 | rel_promoted_bytes: "Promoted", 293 | rel_retained_bytes: "Retained", 294 | rel_cleared_bytes: "Cleared", 295 | }; 296 | 297 | const memoryAmountSource = 298 | [ 299 | [ 300 | {title: "Promoted to the old generation", dataKeys: ["promoted_bytes"]}, 301 | {title: "Retained for another GC run", dataKeys: ["retained_bytes"]}, 302 | {title: "Cleared from the nursery", dataKeys: ["cleared_bytes"]}, 303 | ], 304 | [{title: "Promoted, Kept, Freed", dataKeys: ["promoted_bytes", "retained_bytes", "cleared_bytes"]}], 305 | [{title: "Percentages Promoted, Kept, Freed", dataKeys: ["rel_promoted_bytes", "rel_retained_bytes", "rel_cleared_bytes"]}], 306 | ][stackedBarMode]; 307 | 308 | const barGraphWidth = 309 | dataToUse.length < 10 310 | ? (10 * dataToUse.length) + "%" 311 | : "100%"; 312 | 313 | return ( 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 |

Time spent per GC run

322 | 323 | 324 | 325 | numberFormatter(num / 1000)}/> 326 | }/> 327 | 328 | 329 |
Total Time: { timeToHuman(totalTime) }
330 |

Time between GC runs

331 | 332 | 333 | 334 | numberFormatter(num / 1000)}/> 335 | { 336 | const outer = stuff.payload; 337 | if (typeof outer !== "undefined" && outer !== null && outer.length > 0) { 338 | const payload = outer[0].payload; 339 | return ( 340 |
341 | {payload.sequence_num}:
342 | {timeToHuman(payload.earliest_start_time, "ms since start")}
343 | {timeToHuman(payload.time_since_prev, "ms since previous run")}
344 | {timeToHuman(payload.max_time, "ms time spent")}
345 |
); 346 | } 347 | } 348 | } /> 349 |
350 |
351 |

Amounts of Data

352 | 353 | 354 | 355 | 356 | 357 | { 358 | memoryAmountSource.map(({title, dataKeys}) => ( 359 | 360 |

{ title }

361 | 362 | 363 | { 364 | stackedBarMode !== 2 && numberFormatter(num / 1024)}/> 365 | } 366 | { 367 | dataKeys.map(key => ( 368 | 369 | )) 370 | } 371 | { 372 | const outer = stuff.payload; 373 | if (typeof outer !== "undefined" && outer !== null && outer.length > 0) { 374 | const payload = outer[0].payload; 375 | return ( 376 |
377 | {payload.sequence_num}:
378 | { 379 | stackedBarMode === 2 380 | ? 381 | dataKeys.map(key => ( 382 | {tooltipTextForDataKey[key]}: {numberFormatter(payload[key] * 100, 2)}%
383 | )) 384 | : 385 | dataKeys.map(key => ( 386 | {tooltipTextForDataKey[key]}
387 | )) 388 | } 389 |
); 390 | } 391 | } 392 | } /> 393 | 394 |
395 |
396 |
397 | )) 398 | } 399 |
400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 418 | 419 | 420 |
GC RunThreadsTimingStart Time
421 |
422 | ) 423 | } 424 | --------------------------------------------------------------------------------