Skip to content

Commit

Permalink
Merge 3b0c646 into b26cdb5
Browse files Browse the repository at this point in the history
  • Loading branch information
jlfwong committed Aug 4, 2020
2 parents b26cdb5 + 3b0c646 commit 99de609
Show file tree
Hide file tree
Showing 20 changed files with 829 additions and 362 deletions.
69 changes: 69 additions & 0 deletions src/lib/canvas-2d-batch-renderers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// This file contains a collection of classes which make it easier to perform
// batch rendering of Canvas2D primitives. The advantage of this over just doing
// ctx.beginPath() ... ctx.rect(...) ... ctx.endPath() is that you can construct
// several different batch renderers are the same time, then decide on their
// paint order at the end.
//
// See FlamechartPanZoomView.renderOverlays for an example of how this is used.

export interface TextArgs {
text: string
x: number
y: number
}

export class BatchCanvasTextRenderer {
private argsBatch: TextArgs[] = []

text(args: TextArgs) {
this.argsBatch.push(args)
}

fill(ctx: CanvasRenderingContext2D, color: string) {
if (this.argsBatch.length === 0) return
ctx.fillStyle = color
for (let args of this.argsBatch) {
ctx.fillText(args.text, args.x, args.y)
}
this.argsBatch = []
}
}

export interface RectArgs {
x: number
y: number
w: number
h: number
}

export class BatchCanvasRectRenderer {
private argsBatch: RectArgs[] = []

rect(args: RectArgs) {
this.argsBatch.push(args)
}

private drawPath(ctx: CanvasRenderingContext2D) {
ctx.beginPath()
for (let args of this.argsBatch) {
ctx.rect(args.x, args.y, args.w, args.h)
}
ctx.closePath()
this.argsBatch = []
}

fill(ctx: CanvasRenderingContext2D, color: string) {
if (this.argsBatch.length === 0) return
ctx.fillStyle = color
this.drawPath(ctx)
ctx.fill()
}

stroke(ctx: CanvasRenderingContext2D, color: string, lineWidth: number) {
if (this.argsBatch.length === 0) return
ctx.strokeStyle = color
ctx.lineWidth = lineWidth
this.drawPath(ctx)
ctx.stroke()
}
}
23 changes: 22 additions & 1 deletion src/lib/flamechart.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Frame, CallTreeNode} from './profile'

import {lastOf} from './utils'
import {clamp} from './math'
import {clamp, Rect, Vec2} from './math'

export interface FlamechartFrame {
node: CallTreeNode
Expand Down Expand Up @@ -90,6 +90,27 @@ export class Flamechart {
return clamp(viewportWidth, minWidth, maxWidth)
}

// Given a desired config-space viewport rectangle, clamp the rectangle so
// that it fits within the given flamechart. This prevents the viewport from
// extending past the bounds of the flamechart or zooming in too far.
getClampedConfigSpaceViewportRect({
configSpaceViewportRect,
renderInverted,
}: {
configSpaceViewportRect: Rect
renderInverted?: boolean
}) {
const configSpaceSize = new Vec2(this.getTotalWeight(), this.getLayers().length)
const width = this.getClampedViewportWidth(configSpaceViewportRect.size.x)
const size = configSpaceViewportRect.size.withX(width)
const origin = Vec2.clamp(
configSpaceViewportRect.origin,
new Vec2(0, renderInverted ? 0 : -1),
Vec2.max(Vec2.zero, configSpaceSize.minus(size).plus(new Vec2(0, 1))),
)
return new Rect(origin, configSpaceViewportRect.size.withX(width))
}

constructor(private source: FlamechartDataSource) {
const stack: FlamechartFrame[] = []
const openFrame = (node: CallTreeNode, value: number) => {
Expand Down
90 changes: 90 additions & 0 deletions src/lib/profile-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {Profile, Frame, CallTreeNode} from './profile'
import {FuzzyMatch, fuzzyMatchStrings} from './fuzzy-find'
import {Flamechart, FlamechartFrame} from './flamechart'
import {Rect, Vec2} from './math'

export enum FlamechartType {
CHRONO_FLAME_CHART,
LEFT_HEAVY_FLAME_GRAPH,
}

// A utility class for storing cached search results to avoid recomputation when
// the search results & profile did not change.
export class ProfileSearchResults {
constructor(readonly profile: Profile, readonly searchQuery: string) {}

private matches: Map<Frame, FuzzyMatch> | null = null
getMatchForFrame(frame: Frame): FuzzyMatch | null {
if (!this.matches) {
this.matches = new Map()
this.profile.forEachFrame(frame => {
const match = fuzzyMatchStrings(frame.name, this.searchQuery)
if (match == null) return
this.matches!.set(frame, match)
})
}
return this.matches.get(frame) || null
}
}

export interface FlamechartSearchMatch {
configSpaceBounds: Rect
node: CallTreeNode
}

interface CachedFlamechartResult {
matches: FlamechartSearchMatch[]
indexForNode: Map<CallTreeNode, number>
}

export class FlamechartSearchResults {
constructor(readonly flamechart: Flamechart, readonly profileResults: ProfileSearchResults) {}

private matches: CachedFlamechartResult | null = null
private getResults(): CachedFlamechartResult {
if (this.matches == null) {
const matches: FlamechartSearchMatch[] = []
const indexForNode = new Map<CallTreeNode, number>()
const visit = (frame: FlamechartFrame, depth: number) => {
const {node} = frame
if (this.profileResults.getMatchForFrame(node.frame)) {
const configSpaceBounds = new Rect(
new Vec2(frame.start, depth),
new Vec2(frame.end - frame.start, 1),
)
indexForNode.set(node, matches.length)
matches.push({configSpaceBounds, node})
}

frame.children.forEach(child => {
visit(child, depth + 1)
})
}

const layers = this.flamechart.getLayers()
if (layers.length > 0) {
layers[0].forEach(frame => visit(frame, 0))
}

this.matches = {matches, indexForNode}
}
return this.matches
}

count(): number {
return this.getResults().matches.length
}

indexOf(node: CallTreeNode): number | null {
const result = this.getResults().indexForNode.get(node)
return result === undefined ? null : result
}

at(index: number): FlamechartSearchMatch {
const matches = this.getResults().matches
if (index < 0 || index >= matches.length) {
throw new Error(`Index ${index} out of bounds in list of ${matches.length} matches.`)
}
return matches[index]
}
}
31 changes: 21 additions & 10 deletions src/lib/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ export class Profile {

protected frames = new KeyedSet<Frame>()

// Profiles store two call-trees.
//
// The "append order" call tree is the one in which nodes are ordered in
// whatever order they were appended to their parent.
//
// The "grouped" call tree is one in which each node has at most one child per
// frame. Nodes are ordered in decreasing order of weight
protected appendOrderCalltreeRoot = new CallTreeNode(Frame.root, null)
protected groupedCalltreeRoot = new CallTreeNode(Frame.root, null)

Expand Down Expand Up @@ -169,6 +176,17 @@ export class Profile {
return this.totalNonIdleWeight
}

// This is private because it should only be called in the ProfileBuilder
// classes. Once a Profile instance has been constructed, it should be treated
// as immutable.
protected sortGroupedCallTree() {
function visit(node: CallTreeNode) {
node.children.sort((a, b) => -(a.getTotalWeight() - b.getTotalWeight()))
node.children.forEach(visit)
}
visit(this.groupedCalltreeRoot)
}

forEachCallGrouped(
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (node: CallTreeNode, value: number) => void,
Expand All @@ -180,10 +198,7 @@ export class Profile {

let childTime = 0

const children = [...node.children]
children.sort((a, b) => -(a.getTotalWeight() - b.getTotalWeight()))

children.forEach(function (child) {
node.children.forEach(function (child) {
visit(child, start + childTime)
childTime += child.getTotalWeight()
})
Expand Down Expand Up @@ -250,12 +265,6 @@ export class Profile {
this.frames.forEach(fn)
}

forEachSample(fn: (sample: CallTreeNode, weight: number) => void) {
for (let i = 0; i < this.samples.length; i++) {
fn(this.samples[i], this.weights[i])
}
}

getProfileWithRecursionFlattened(): Profile {
const builder = new CallTreeProfileBuilder()

Expand Down Expand Up @@ -511,6 +520,7 @@ export class StackListProfileBuilder extends Profile {
this.totalWeight,
this.weights.reduce((a, b) => a + b, 0),
)
this.sortGroupedCallTree()
return this
}
}
Expand Down Expand Up @@ -651,6 +661,7 @@ export class CallTreeProfileBuilder extends Profile {
if (this.appendOrderStack.length > 1 || this.groupedOrderStack.length > 1) {
throw new Error('Tried to complete profile construction with a non-empty stack')
}
this.sortGroupedCallTree()
return this
}
}
31 changes: 31 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {HashParams, getHashParams} from '../lib/hash-params'
import {ProfileGroupState, profileGroup} from './profiles-state'
import {SortMethod, SortField, SortDirection} from '../views/profile-table-view'
import {useSelector} from '../lib/preact-redux'
import {Profile} from '../lib/profile'
import {FlamechartViewState} from './flamechart-view-state'
import {SandwichViewState} from './sandwich-view-state'
import {getProfileToView} from './getters'

export const enum ViewMode {
CHRONO_FLAME_CHART,
Expand Down Expand Up @@ -101,3 +105,30 @@ export function useAppSelector<T>(selector: (t: ApplicationState) => T, cacheArg
/* eslint-disable react-hooks/exhaustive-deps */
return useSelector(selector, cacheArgs)
}

export interface ActiveProfileState {
profile: Profile
index: number
chronoViewState: FlamechartViewState
leftHeavyViewState: FlamechartViewState
sandwichViewState: SandwichViewState
}

export function useActiveProfileState(): ActiveProfileState | null {
return useAppSelector(state => {
const {profileGroup} = state
if (!profileGroup) return null
if (profileGroup.indexToView >= profileGroup.profiles.length) return null

const index = profileGroup.indexToView
const profileState = profileGroup.profiles[index]
return {
...profileGroup.profiles[profileGroup.indexToView],
profile: getProfileToView({
profile: profileState.profile,
flattenRecursion: state.flattenRecursion,
}),
index: profileGroup.indexToView,
}
}, [])
}
52 changes: 19 additions & 33 deletions src/views/application-container.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {h} from 'preact'
import {Application, ActiveProfileState} from './application'
import {getProfileToView, getCanvasContext} from '../store/getters'
import {Application} from './application'
import {getCanvasContext} from '../store/getters'
import {actions} from '../store/actions'
import {useActionCreator} from '../lib/preact-redux'
import {memo} from 'preact/compat'
import {useAppSelector} from '../store'
import {useAppSelector, useActiveProfileState} from '../store'
import {ProfileSearchContextProvider} from './search-view'

const {
setLoading,
Expand All @@ -24,36 +25,21 @@ export const ApplicationContainer = memo(() => {
[],
)

const activeProfileState: ActiveProfileState | null = useAppSelector(state => {
const {profileGroup} = state
if (!profileGroup) return null
if (profileGroup.indexToView >= profileGroup.profiles.length) return null

const index = profileGroup.indexToView
const profileState = profileGroup.profiles[index]
return {
...profileGroup.profiles[profileGroup.indexToView],
profile: getProfileToView({
profile: profileState.profile,
flattenRecursion: state.flattenRecursion,
}),
index: profileGroup.indexToView,
}
}, [])

return (
<Application
activeProfileState={activeProfileState}
canvasContext={canvasContext}
setGLCanvas={useActionCreator(setGLCanvas, [])}
setLoading={useActionCreator(setLoading, [])}
setError={useActionCreator(setError, [])}
setProfileGroup={useActionCreator(setProfileGroup, [])}
setDragActive={useActionCreator(setDragActive, [])}
setViewMode={useActionCreator(setViewMode, [])}
setFlattenRecursion={useActionCreator(setFlattenRecursion, [])}
setProfileIndexToView={useActionCreator(setProfileIndexToView, [])}
{...appState}
/>
<ProfileSearchContextProvider>
<Application
activeProfileState={useActiveProfileState()}
canvasContext={canvasContext}
setGLCanvas={useActionCreator(setGLCanvas, [])}
setLoading={useActionCreator(setLoading, [])}
setError={useActionCreator(setError, [])}
setProfileGroup={useActionCreator(setProfileGroup, [])}
setDragActive={useActionCreator(setDragActive, [])}
setViewMode={useActionCreator(setViewMode, [])}
setFlattenRecursion={useActionCreator(setFlattenRecursion, [])}
setProfileIndexToView={useActionCreator(setProfileIndexToView, [])}
{...appState}
/>
</ProfileSearchContextProvider>
)
})

0 comments on commit 99de609

Please sign in to comment.