123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- import {useEffect} from 'react';
- import {browserHistory} from 'react-router';
- import {Query} from 'history';
- import * as qs from 'query-string';
- import {DeepPartial} from 'sentry/types/utils';
- import {useFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
- import {Rect} from 'sentry/utils/profiling/gl/utils';
- import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
- import {useLocation} from 'sentry/utils/useLocation';
- import {DEFAULT_FLAMEGRAPH_STATE, FlamegraphState} from './flamegraphContext';
- // Intersect the types so we can properly guard
- type PossibleQuery =
- | Query
- | (Pick<FlamegraphState['preferences'], 'colorCoding' | 'sorting' | 'view'> &
- Pick<FlamegraphState['search'], 'query'>);
- function isColorCoding(
- value: PossibleQuery['colorCoding'] | FlamegraphState['preferences']['colorCoding']
- ): value is FlamegraphState['preferences']['colorCoding'] {
- if (typeof value !== 'string') {
- return false;
- }
- return (
- value === 'by symbol name' ||
- value === 'by system frame' ||
- value === 'by application frame' ||
- value === 'by library' ||
- value === 'by recursion' ||
- value === 'by frequency'
- );
- }
- function isLayout(
- value: PossibleQuery['colorCoding'] | FlamegraphState['preferences']['colorCoding']
- ): value is FlamegraphState['preferences']['layout'] {
- if (typeof value !== 'string') {
- return false;
- }
- return value === 'table right' || value === 'table bottom' || value === 'table left';
- }
- function isSorting(
- value: PossibleQuery['sorting'] | FlamegraphState['preferences']['sorting']
- ): value is FlamegraphState['preferences']['sorting'] {
- if (typeof value !== 'string') {
- return false;
- }
- return value === 'left heavy' || value === 'call order';
- }
- function isView(
- value: PossibleQuery['view'] | FlamegraphState['preferences']['view']
- ): value is FlamegraphState['preferences']['view'] {
- if (typeof value !== 'string') {
- return false;
- }
- return value === 'top down' || value === 'bottom up';
- }
- export function decodeFlamegraphStateFromQueryParams(
- query: qs.ParsedQuery
- ): DeepPartial<FlamegraphState> {
- const decoded: DeepPartial<FlamegraphState> = {};
- // Similarly to how we encode frame name and values, we want to
- // omit the field entirely if it is not present in the query string or
- // if it is an empty string.
- if (typeof query.frameName === 'string') {
- decoded.profiles = {
- ...(decoded.profiles ?? {}),
- highlightFrames: {
- ...(decoded.profiles?.highlightFrames ?? {}),
- name: query.frameName ? query.frameName : undefined,
- },
- };
- }
- if (typeof query.framePackage === 'string') {
- decoded.profiles = {
- ...(decoded.profiles ?? {}),
- highlightFrames: {
- ...(decoded.profiles?.highlightFrames ?? {}),
- package: query.framePackage ? query.framePackage : undefined,
- },
- };
- }
- if (typeof query.tid === 'string' && !isNaN(parseInt(query.tid, 10))) {
- decoded.profiles = {
- ...(decoded.profiles ?? {}),
- threadId: parseInt(query.tid, 10),
- };
- }
- const fov = Rect.decode(query.fov);
- if (fov) {
- decoded.position = {view: fov};
- }
- decoded.preferences = {};
- decoded.search = {};
- if (isLayout(query.layout)) {
- decoded.preferences.layout = query.layout;
- }
- if (isColorCoding(query.colorCoding)) {
- decoded.preferences.colorCoding = query.colorCoding;
- }
- if (isSorting(query.sorting)) {
- decoded.preferences.sorting = query.sorting;
- }
- if (isView(query.view)) {
- decoded.preferences.view = query.view;
- }
- if (typeof query.query === 'string') {
- decoded.search.query = query.query;
- }
- return decoded;
- }
- export function encodeFlamegraphStateToQueryParams(state: FlamegraphState) {
- const highlightFrameToEncode: Record<string, string> = {};
- // For some frames we do not have a package (or name) if that happens we want to omit
- // the field entirely from the query string. This is to avoid default values being used
- // as qs.parse will initialize empty values to "" which can differ from the respective
- // frame values which are undefined.
- if (state.profiles.highlightFrames?.name) {
- highlightFrameToEncode.frameName = state.profiles.highlightFrames.name;
- }
- if (state.profiles.highlightFrames?.package) {
- highlightFrameToEncode.framePackage = state.profiles.highlightFrames.package;
- }
- const highlightFrame = state.profiles.highlightFrames
- ? {
- frameName: state.profiles.highlightFrames?.name,
- framePackage: state.profiles.highlightFrames?.package,
- }
- : {};
- return {
- colorCoding: state.preferences.colorCoding,
- sorting: state.preferences.sorting,
- view: state.preferences.view,
- query: state.search.query,
- ...highlightFrame,
- ...(state.position.view.isEmpty()
- ? {fov: undefined}
- : {fov: Rect.encode(state.position.view)}),
- ...(typeof state.profiles.threadId === 'number'
- ? {tid: state.profiles.threadId}
- : {}),
- };
- }
- function maybeOmitHighlightedFrame(query: Query, state: FlamegraphState) {
- if (!state.profiles.highlightFrames && query.frameName && query.framePackage) {
- const {frameName: _, framePackage: __, ...rest} = query;
- return rest;
- }
- return query;
- }
- export function FlamegraphStateQueryParamSync() {
- const location = useLocation();
- const [state] = useFlamegraphState();
- useEffect(() => {
- browserHistory.replace({
- ...location,
- query: {
- ...maybeOmitHighlightedFrame(location.query, state),
- ...encodeFlamegraphStateToQueryParams(state),
- },
- });
- // We only want to sync the query params when the state changes
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
- return null;
- }
- export const FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY = 'flamegraph-preferences';
- export function FlamegraphStateLocalStorageSync() {
- const [state] = useFlamegraphState();
- const [_, setState] = useLocalStorageState<DeepPartial<FlamegraphState>>(
- FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY,
- {
- preferences: {
- layout: DEFAULT_FLAMEGRAPH_STATE.preferences.layout,
- timelines: DEFAULT_FLAMEGRAPH_STATE.preferences.timelines,
- view: DEFAULT_FLAMEGRAPH_STATE.preferences.view,
- },
- }
- );
- useEffect(() => {
- setState({
- preferences: {
- layout: state.preferences.layout,
- timelines: state.preferences.timelines,
- view: state.preferences.view,
- },
- });
- // We only want to sync the local storage when the state changes
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state.preferences.layout, state.preferences.timelines, state.preferences.view]);
- return null;
- }
|