github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/redux/reducers/tracing.ts (about)

     1  import { createSlice, PayloadAction } from '@reduxjs/toolkit';
     2  import type { Profile } from '@pyroscope/models/src';
     3  import {
     4    MergeOutput,
     5    mergeWithQueryID,
     6    HeatmapOutput,
     7    getHeatmap,
     8    SelectionProfileOutput,
     9    getHeatmapSelectionProfile,
    10    Heatmap,
    11    getHeatmapProps,
    12    selectionProfileProps,
    13  } from '@webapp/services/render';
    14  import type { RootState } from '@webapp/redux/store';
    15  import { RequestAbortedError } from '@webapp/services/base';
    16  import { addNotification } from './notifications';
    17  import { createAsyncThunk } from '../async-thunk';
    18  
    19  type MergeMetadata = {
    20    appName: string;
    21    startTime: string;
    22    endTime: string;
    23    profilesLength: number;
    24  };
    25  
    26  type SingleView =
    27    | { type: 'pristine'; profile?: Profile; mergeMetadata?: MergeMetadata }
    28    | { type: 'loading'; profile?: Profile; mergeMetadata?: MergeMetadata }
    29    | {
    30        type: 'loaded';
    31        profile: Profile;
    32        mergeMetadata: MergeMetadata;
    33      }
    34    | {
    35        type: 'reloading';
    36        profile: Profile;
    37        mergeMetadata: MergeMetadata;
    38      };
    39  // TODO
    40  
    41  type ExemplarsSingleView =
    42    | {
    43        type: 'pristine';
    44        heatmap?: Heatmap | null;
    45        profile?: Profile;
    46        selectionProfile?: Profile;
    47      }
    48    | {
    49        type: 'loading';
    50        heatmap?: Heatmap | null;
    51        profile?: Profile;
    52        selectionProfile?: Profile;
    53      }
    54    | {
    55        type: 'loaded';
    56        heatmap: Heatmap | null;
    57        profile?: Profile;
    58        selectionProfile?: Profile;
    59      }
    60    | {
    61        type: 'reloading';
    62        heatmap: Heatmap | null;
    63        profile?: Profile;
    64        selectionProfile?: Profile;
    65      };
    66  
    67  interface TracingState {
    68    queryID: string;
    69    maxNodes: string;
    70    refreshToken?: string;
    71  
    72    exemplarsSingleView: ExemplarsSingleView;
    73    singleView: SingleView;
    74  }
    75  
    76  let singleViewAbortController: AbortController | undefined;
    77  let exemplarsSingleViewAbortController: AbortController | undefined;
    78  let selectionProfileAbortController: AbortController | undefined;
    79  
    80  const initialState: TracingState = {
    81    queryID: '',
    82    maxNodes: '1024',
    83  
    84    exemplarsSingleView: { type: 'pristine' },
    85    singleView: { type: 'pristine' },
    86  };
    87  
    88  export const fetchSingleView = createAsyncThunk<
    89    MergeOutput,
    90    null,
    91    { state: { tracing: TracingState } }
    92  >('tracing/singleView', async (_, thunkAPI) => {
    93    if (singleViewAbortController) {
    94      singleViewAbortController.abort();
    95    }
    96  
    97    singleViewAbortController = new AbortController();
    98    thunkAPI.signal = singleViewAbortController.signal;
    99  
   100    const state = thunkAPI.getState();
   101    const res = await mergeWithQueryID(state.tracing, singleViewAbortController);
   102  
   103    if (res.isOk) {
   104      return Promise.resolve(res.value);
   105    }
   106  
   107    if (res.isErr && res.error instanceof RequestAbortedError) {
   108      return thunkAPI.rejectWithValue({ rejectedWithValue: 'reloading' });
   109    }
   110  
   111    thunkAPI.dispatch(
   112      addNotification({
   113        type: 'danger',
   114        title: 'Failed to load single view data',
   115        message: res.error.message,
   116      })
   117    );
   118  
   119    return Promise.reject(res.error);
   120  });
   121  
   122  export const fetchExemplarsSingleView = createAsyncThunk<
   123    HeatmapOutput,
   124    getHeatmapProps,
   125    { state: { tracing: TracingState } }
   126  >('tracing/exemplarsSingleView', async (heatmapProps, thunkAPI) => {
   127    if (exemplarsSingleViewAbortController) {
   128      exemplarsSingleViewAbortController.abort();
   129    }
   130  
   131    exemplarsSingleViewAbortController = new AbortController();
   132    thunkAPI.signal = exemplarsSingleViewAbortController.signal;
   133  
   134    const res = await getHeatmap(
   135      heatmapProps,
   136      exemplarsSingleViewAbortController
   137    );
   138  
   139    if (res.isOk) {
   140      return Promise.resolve(res.value);
   141    }
   142  
   143    if (res.isErr && res.error instanceof RequestAbortedError) {
   144      return thunkAPI.rejectWithValue({ rejectedWithValue: 'reloading' });
   145    }
   146  
   147    thunkAPI.dispatch(
   148      addNotification({
   149        type: 'danger',
   150        title: 'Failed to load heatmap',
   151        message: res.error.message,
   152      })
   153    );
   154  
   155    return Promise.reject(res.error);
   156  });
   157  
   158  export const fetchSelectionProfile = createAsyncThunk<
   159    SelectionProfileOutput,
   160    selectionProfileProps,
   161    { state: { tracing: TracingState } }
   162  >('tracing/fetchSelectionProfile', async (selectionProfileProps, thunkAPI) => {
   163    if (selectionProfileAbortController) {
   164      selectionProfileAbortController.abort();
   165    }
   166  
   167    selectionProfileAbortController = new AbortController();
   168    thunkAPI.signal = selectionProfileAbortController.signal;
   169  
   170    const res = await getHeatmapSelectionProfile(
   171      selectionProfileProps,
   172      selectionProfileAbortController
   173    );
   174  
   175    if (res.isOk) {
   176      return Promise.resolve(res.value);
   177    }
   178  
   179    if (res.isErr && res.error instanceof RequestAbortedError) {
   180      return thunkAPI.rejectWithValue({ rejectedWithValue: 'reloading' });
   181    }
   182  
   183    thunkAPI.dispatch(
   184      addNotification({
   185        type: 'danger',
   186        title: 'Failed to load profile',
   187        message: res.error.message,
   188      })
   189    );
   190  
   191    return Promise.reject(res.error);
   192  });
   193  
   194  export const tracingSlice = createSlice({
   195    name: 'tracing',
   196    initialState,
   197    reducers: {
   198      setMaxNodes(state, action: PayloadAction<string>) {
   199        state.maxNodes = action.payload;
   200      },
   201      setQueryID(state, action: PayloadAction<string>) {
   202        state.queryID = action.payload;
   203      },
   204      refresh(state) {
   205        state.refreshToken = Math.random().toString();
   206      },
   207    },
   208    extraReducers: (builder) => {
   209      /*************************/
   210      /*      Single View      */
   211      /*************************/
   212      builder.addCase(fetchSingleView.pending, (state) => {
   213        switch (state.singleView.type) {
   214          // if we are fetching but there's already data
   215          // it's considered a 'reload'
   216          case 'reloading':
   217          case 'loaded': {
   218            state.singleView = {
   219              ...state.singleView,
   220              type: 'reloading',
   221            };
   222            break;
   223          }
   224  
   225          default: {
   226            state.singleView = { type: 'loading' };
   227          }
   228        }
   229      });
   230  
   231      builder.addCase(fetchSingleView.fulfilled, (state, action) => {
   232        state.singleView = {
   233          ...action.payload,
   234          mergeMetadata: action.payload.mergeMetadata,
   235          type: 'loaded',
   236        };
   237      });
   238  
   239      builder.addCase(fetchSingleView.rejected, (state, action) => {
   240        switch (state.singleView.type) {
   241          // if previous state is loaded, let's continue displaying data
   242          case 'reloading': {
   243            let type: SingleView['type'] = 'reloading';
   244            if (action.meta.rejectedWithValue) {
   245              type = (
   246                action?.payload as { rejectedWithValue: SingleView['type'] }
   247              )?.rejectedWithValue;
   248            } else if (action.error.message === 'cancel') {
   249              type = 'loaded';
   250            }
   251            state.singleView = {
   252              ...state.singleView,
   253              type,
   254            };
   255            break;
   256          }
   257  
   258          default: {
   259            // it failed to load for the first time, so far all effects it's pristine
   260            state.singleView = {
   261              type: 'pristine',
   262            };
   263          }
   264        }
   265      });
   266  
   267      /***********************************/
   268      /*      Exemplars Single View      */
   269      /***********************************/
   270  
   271      builder.addCase(fetchExemplarsSingleView.pending, (state) => {
   272        switch (state.exemplarsSingleView.type) {
   273          // if we are fetching but there's already data
   274          // it's considered a 'reload'
   275          case 'reloading':
   276          case 'loaded': {
   277            state.exemplarsSingleView = {
   278              ...state.exemplarsSingleView,
   279              type: 'reloading',
   280            };
   281            break;
   282          }
   283  
   284          default: {
   285            state.exemplarsSingleView = { type: 'loading' };
   286          }
   287        }
   288      });
   289  
   290      builder.addCase(fetchExemplarsSingleView.fulfilled, (state, action) => {
   291        state.exemplarsSingleView = {
   292          ...action.payload,
   293          type: 'loaded',
   294        };
   295      });
   296  
   297      builder.addCase(fetchExemplarsSingleView.rejected, (state, action) => {
   298        switch (state.exemplarsSingleView.type) {
   299          // if previous state is loaded, let's continue displaying data
   300          case 'reloading': {
   301            let type: ExemplarsSingleView['type'] = 'reloading';
   302            if (action.meta.rejectedWithValue) {
   303              type = (
   304                action?.payload as {
   305                  rejectedWithValue: ExemplarsSingleView['type'];
   306                }
   307              )?.rejectedWithValue;
   308            } else if (action.error.message === 'cancel') {
   309              type = 'loaded';
   310            }
   311            state.exemplarsSingleView = {
   312              ...state.exemplarsSingleView,
   313              type,
   314            };
   315            break;
   316          }
   317  
   318          default: {
   319            // it failed to load for the first time, so far all effects it's pristine
   320            state.exemplarsSingleView = {
   321              type: 'pristine',
   322            };
   323          }
   324        }
   325      });
   326  
   327      /**************************************/
   328      /*      Heatmap Selection Profile      */
   329      /**************************************/
   330  
   331      builder.addCase(fetchSelectionProfile.pending, (state) => {
   332        switch (state.exemplarsSingleView.type) {
   333          // if we are fetching but there's already data
   334          // it's considered a 'reload'
   335          case 'reloading':
   336          case 'loaded': {
   337            state.exemplarsSingleView = {
   338              ...state.exemplarsSingleView,
   339              type: 'reloading',
   340            };
   341            break;
   342          }
   343  
   344          default: {
   345            state.exemplarsSingleView = { type: 'loading' };
   346          }
   347        }
   348      });
   349  
   350      builder.addCase(fetchSelectionProfile.fulfilled, (state, action) => {
   351        state.exemplarsSingleView.type = 'loaded';
   352        state.exemplarsSingleView.selectionProfile =
   353          action.payload.selectionProfile;
   354      });
   355  
   356      builder.addCase(fetchSelectionProfile.rejected, (state, action) => {
   357        switch (state.exemplarsSingleView.type) {
   358          // if previous state is loaded, let's continue displaying data
   359          case 'reloading': {
   360            let type: ExemplarsSingleView['type'] = 'reloading';
   361            if (action.meta.rejectedWithValue) {
   362              type = (
   363                action?.payload as {
   364                  rejectedWithValue: ExemplarsSingleView['type'];
   365                }
   366              )?.rejectedWithValue;
   367            } else if (action.error.message === 'cancel') {
   368              type = 'loaded';
   369            }
   370            state.exemplarsSingleView = {
   371              ...state.exemplarsSingleView,
   372              type,
   373            };
   374            break;
   375          }
   376  
   377          default: {
   378            // it failed to load for the first time, so far all effects it's pristine
   379            state.exemplarsSingleView = {
   380              type: 'pristine',
   381            };
   382          }
   383        }
   384      });
   385    },
   386  });
   387  
   388  export const selectTracingState = (state: RootState) => state.tracing;
   389  
   390  export default tracingSlice.reducer;
   391  export const { actions } = tracingSlice;