• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2018 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {Draft} from 'immer';
16
17import {assertExists} from '../base/logging';
18import {ConvertTrace} from '../controller/trace_converter';
19
20import {
21  createEmptyState,
22  LogsPagination,
23  RecordConfig,
24  SCROLLING_TRACK_GROUP,
25  State,
26  Status,
27  TraceTime,
28} from './state';
29
30type StateDraft = Draft<State>;
31
32
33function clearTraceState(state: StateDraft) {
34  const nextId = state.nextId;
35  const recordConfig = state.recordConfig;
36  const route = state.route;
37
38  Object.assign(state, createEmptyState());
39  state.nextId = nextId;
40  state.recordConfig = recordConfig;
41  state.route = route;
42}
43
44export const StateActions = {
45
46  navigate(state: StateDraft, args: {route: string}): void {
47    state.route = args.route;
48  },
49
50  openTraceFromFile(state: StateDraft, args: {file: File}): void {
51    clearTraceState(state);
52    const id = `${state.nextId++}`;
53    state.engines[id] = {
54      id,
55      ready: false,
56      source: args.file,
57    };
58    state.route = `/viewer`;
59  },
60
61  convertTraceToJson(_: StateDraft, args: {file: File}): void {
62    ConvertTrace(args.file);
63  },
64
65  openTraceFromUrl(state: StateDraft, args: {url: string}): void {
66    clearTraceState(state);
67    const id = `${state.nextId++}`;
68    state.engines[id] = {
69      id,
70      ready: false,
71      source: args.url,
72    };
73    state.route = `/viewer`;
74  },
75
76  addTrack(state: StateDraft, args: {
77    id?: string; engineId: string; kind: string; name: string;
78    trackGroup?: string;
79    config: {};
80  }): void {
81    const id = args.id !== undefined ? args.id : `${state.nextId++}`;
82    state.tracks[id] = {
83      id,
84      engineId: args.engineId,
85      kind: args.kind,
86      name: args.name,
87      trackGroup: args.trackGroup,
88      config: args.config,
89    };
90    if (args.trackGroup === SCROLLING_TRACK_GROUP) {
91      state.scrollingTracks.push(id);
92    } else if (args.trackGroup !== undefined) {
93      assertExists(state.trackGroups[args.trackGroup]).tracks.push(id);
94    }
95  },
96
97  addTrackGroup(
98      state: StateDraft,
99      // Define ID in action so a track group can be referred to without running
100      // the reducer.
101      args: {
102        engineId: string; name: string; id: string; summaryTrackId: string;
103        collapsed: boolean;
104      }): void {
105    state.trackGroups[args.id] = {
106      ...args,
107      tracks: [],
108    };
109  },
110
111  reqTrackData(state: StateDraft, args: {
112    trackId: string; start: number; end: number; resolution: number;
113  }): void {
114    const id = args.trackId;
115    state.tracks[id].dataReq = {
116      start: args.start,
117      end: args.end,
118      resolution: args.resolution
119    };
120  },
121
122  clearTrackDataReq(state: StateDraft, args: {trackId: string}): void {
123    const id = args.trackId;
124    state.tracks[id].dataReq = undefined;
125  },
126
127  executeQuery(
128      state: StateDraft,
129      args: {queryId: string; engineId: string; query: string}): void {
130    state.queries[args.queryId] = {
131      id: args.queryId,
132      engineId: args.engineId,
133      query: args.query,
134    };
135  },
136
137  deleteQuery(state: StateDraft, args: {queryId: string}): void {
138    delete state.queries[args.queryId];
139  },
140
141  moveTrack(
142      state: StateDraft,
143      args: {srcId: string; op: 'before' | 'after', dstId: string}): void {
144    const moveWithinTrackList = (trackList: string[]) => {
145      const newList: string[] = [];
146      for (let i = 0; i < trackList.length; i++) {
147        const curTrackId = trackList[i];
148        if (curTrackId === args.dstId && args.op === 'before') {
149          newList.push(args.srcId);
150        }
151        if (curTrackId !== args.srcId) {
152          newList.push(curTrackId);
153        }
154        if (curTrackId === args.dstId && args.op === 'after') {
155          newList.push(args.srcId);
156        }
157      }
158      trackList.splice(0);
159      newList.forEach(x => {
160        trackList.push(x);
161      });
162    };
163
164    moveWithinTrackList(state.pinnedTracks);
165    moveWithinTrackList(state.scrollingTracks);
166  },
167
168  toggleTrackPinned(state: StateDraft, args: {trackId: string}): void {
169    const id = args.trackId;
170    const isPinned = state.pinnedTracks.includes(id);
171    const trackGroup = assertExists(state.tracks[id]).trackGroup;
172
173    if (isPinned) {
174      state.pinnedTracks.splice(state.pinnedTracks.indexOf(id), 1);
175      if (trackGroup === SCROLLING_TRACK_GROUP) {
176        state.scrollingTracks.unshift(id);
177      }
178    } else {
179      if (trackGroup === SCROLLING_TRACK_GROUP) {
180        state.scrollingTracks.splice(state.scrollingTracks.indexOf(id), 1);
181      }
182      state.pinnedTracks.push(id);
183    }
184  },
185
186  toggleTrackGroupCollapsed(state: StateDraft, args: {trackGroupId: string}):
187      void {
188        const id = args.trackGroupId;
189        const trackGroup = assertExists(state.trackGroups[id]);
190        trackGroup.collapsed = !trackGroup.collapsed;
191      },
192
193  setEngineReady(state: StateDraft, args: {engineId: string; ready: boolean}):
194      void {
195        state.engines[args.engineId].ready = args.ready;
196      },
197
198  createPermalink(state: StateDraft, _: {}): void {
199    state.permalink = {requestId: `${state.nextId++}`, hash: undefined};
200  },
201
202  setPermalink(state: StateDraft, args: {requestId: string; hash: string}):
203      void {
204        // Drop any links for old requests.
205        if (state.permalink.requestId !== args.requestId) return;
206        state.permalink = args;
207      },
208
209  loadPermalink(state: StateDraft, args: {hash: string}): void {
210    state.permalink = {
211      requestId: `${state.nextId++}`,
212      hash: args.hash,
213    };
214  },
215
216  clearPermalink(state: StateDraft, _: {}): void {
217    state.permalink = {};
218  },
219
220  setTraceTime(state: StateDraft, args: TraceTime): void {
221    state.traceTime = args;
222  },
223
224  setVisibleTraceTime(
225      state: StateDraft, args: {time: TraceTime; lastUpdate: number;}): void {
226    state.frontendLocalState.visibleTraceTime = args.time;
227    state.frontendLocalState.lastUpdate = args.lastUpdate;
228  },
229
230  updateStatus(state: StateDraft, args: Status): void {
231    state.status = args;
232  },
233
234  // TODO(hjd): Remove setState - it causes problems due to reuse of ids.
235  setState(state: StateDraft, args: {newState: State}): void {
236    for (const key of Object.keys(state)) {
237      // tslint:disable-next-line no-any
238      delete (state as any)[key];
239    }
240    for (const key of Object.keys(args.newState)) {
241      // tslint:disable-next-line no-any
242      (state as any)[key] = (args.newState as any)[key];
243    }
244  },
245
246  setRecordConfig(state: StateDraft, args: {config: RecordConfig;}): void {
247    state.recordConfig = args.config;
248  },
249
250  selectNote(state: StateDraft, args: {id: string}): void {
251    if (args.id) {
252      state.currentSelection = {
253        kind: 'NOTE',
254        id: args.id
255      };
256    }
257  },
258
259  addNote(state: StateDraft, args: {timestamp: number, color: string}): void {
260    const id = `${state.nextId++}`;
261    state.notes[id] = {
262      id,
263      timestamp: args.timestamp,
264      color: args.color,
265      text: '',
266    };
267    this.selectNote(state, {id});
268  },
269
270  changeNoteColor(state: StateDraft, args: {id: string, newColor: string}):
271      void {
272        const note = state.notes[args.id];
273        if (note === undefined) return;
274        note.color = args.newColor;
275      },
276
277  changeNoteText(state: StateDraft, args: {id: string, newText: string}): void {
278    const note = state.notes[args.id];
279    if (note === undefined) return;
280    note.text = args.newText;
281  },
282
283  removeNote(state: StateDraft, args: {id: string}): void {
284    delete state.notes[args.id];
285    if (state.currentSelection === null) return;
286    if (state.currentSelection.kind === 'NOTE' &&
287        state.currentSelection.id === args.id) {
288      state.currentSelection = null;
289    }
290  },
291
292  selectSlice(state: StateDraft, args: {utid: number, id: number}): void {
293    state.currentSelection = {
294      kind: 'SLICE',
295      utid: args.utid,
296      id: args.id,
297    };
298  },
299
300  selectTimeSpan(
301      state: StateDraft, args: {startTs: number, endTs: number}): void {
302    state.currentSelection = {
303      kind: 'TIMESPAN',
304      startTs: args.startTs,
305      endTs: args.endTs,
306    };
307  },
308
309  selectThreadState(
310      state: StateDraft,
311      args: {utid: number, ts: number, dur: number, state: string}): void {
312    state.currentSelection = {
313      kind: 'THREAD_STATE',
314      utid: args.utid,
315      ts: args.ts,
316      dur: args.dur,
317      state: args.state
318    };
319  },
320
321  deselect(state: StateDraft, _: {}): void {
322    state.currentSelection = null;
323  },
324
325  updateLogsPagination(state: StateDraft, args: LogsPagination): void {
326    state.logsPagination = args;
327  },
328
329};
330
331// When we are on the frontend side, we don't really want to execute the
332// actions above, we just want to serialize them and marshal their
333// arguments, send them over to the controller side and have them being
334// executed there. The magic below takes care of turning each action into a
335// function that returns the marshaled args.
336
337// A DeferredAction is a bundle of Args and a method name. This is the marshaled
338// version of a StateActions method call.
339export interface DeferredAction<Args = {}> {
340  type: string;
341  args: Args;
342}
343
344// This type magic creates a type function DeferredActions<T> which takes a type
345// T and 'maps' its attributes. For each attribute on T matching the signature:
346// (state: StateDraft, args: Args) => void
347// DeferredActions<T> has an attribute:
348// (args: Args) => DeferredAction<Args>
349type ActionFunction<Args> = (state: StateDraft, args: Args) => void;
350type DeferredActionFunc<T> = T extends ActionFunction<infer Args>?
351    (args: Args) => DeferredAction<Args>:
352    never;
353type DeferredActions<C> = {
354  [P in keyof C]: DeferredActionFunc<C[P]>;
355};
356
357// Actions is an implementation of DeferredActions<typeof StateActions>.
358// (since StateActions is a variable not a type we have to do
359// 'typeof StateActions' to access the (unnamed) type of StateActions).
360// It's a Proxy such that any attribute access returns a function:
361// (args) => {return {type: ATTRIBUTE_NAME, args};}
362export const Actions =
363    // tslint:disable-next-line no-any
364    new Proxy<DeferredActions<typeof StateActions>>({} as any, {
365      // tslint:disable-next-line no-any
366      get(_: any, prop: string, _2: any) {
367        return (args: {}): DeferredAction<{}> => {
368          return {
369            type: prop,
370            args,
371          };
372        };
373      },
374    });
375