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 {produce} from 'immer';
16
17import {SLICE_TRACK_KIND} from '../tracks/chrome_slices/common';
18import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile/common';
19import {
20  PROCESS_SCHEDULING_TRACK_KIND
21} from '../tracks/process_scheduling/common';
22import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
23
24import {StateActions} from './actions';
25import {
26  createEmptyState,
27  SCROLLING_TRACK_GROUP,
28  State,
29  TraceUrlSource,
30  TrackKindPriority,
31} from './state';
32
33function fakeTrack(state: State, args: {
34  id: string,
35  kind?: string,
36  trackGroup?: string,
37  trackKindPriority?: TrackKindPriority,
38  name?: string,
39  tid?: string
40}): State {
41  return produce(state, draft => {
42    StateActions.addTrack(draft, {
43      id: args.id,
44      engineId: '0',
45      kind: args.kind || 'SOME_TRACK_KIND',
46      name: args.name || 'A track',
47      trackKindPriority: args.trackKindPriority === undefined ?
48          TrackKindPriority.ORDINARY :
49          args.trackKindPriority,
50      trackGroup: args.trackGroup || SCROLLING_TRACK_GROUP,
51      config: {tid: args.tid || '0'}
52    });
53  });
54}
55
56function fakeTrackGroup(
57    state: State, args: {id: string, summaryTrackId: string}): State {
58  return produce(state, draft => {
59    StateActions.addTrackGroup(draft, {
60      name: 'A group',
61      id: args.id,
62      engineId: '0',
63      collapsed: false,
64      summaryTrackId: args.summaryTrackId
65    });
66  });
67}
68
69function pinnedAndScrollingTracks(
70    state: State,
71    ids: string[],
72    pinnedTracks: string[],
73    scrollingTracks: string[]): State {
74  for (const id of ids) {
75    state = fakeTrack(state, {id});
76  }
77  state = produce(state, draft => {
78    draft.pinnedTracks = pinnedTracks;
79    draft.scrollingTracks = scrollingTracks;
80  });
81  return state;
82}
83
84test('navigate', () => {
85  const after = produce(createEmptyState(), draft => {
86    StateActions.navigate(draft, {route: '/foo'});
87  });
88  expect(after.route).toBe('/foo');
89});
90
91test('add scrolling tracks', () => {
92  const once = produce(createEmptyState(), draft => {
93    StateActions.addTrack(draft, {
94      engineId: '1',
95      kind: 'cpu',
96      name: 'Cpu 1',
97      trackKindPriority: TrackKindPriority.ORDINARY,
98      trackGroup: SCROLLING_TRACK_GROUP,
99      config: {},
100    });
101  });
102  const twice = produce(once, draft => {
103    StateActions.addTrack(draft, {
104      engineId: '2',
105      kind: 'cpu',
106      name: 'Cpu 2',
107      trackKindPriority: TrackKindPriority.ORDINARY,
108      trackGroup: SCROLLING_TRACK_GROUP,
109      config: {},
110    });
111  });
112
113  expect(Object.values(twice.tracks).length).toBe(2);
114  expect(twice.scrollingTracks.length).toBe(2);
115});
116
117test('add track to track group', () => {
118  let state = createEmptyState();
119  state = fakeTrack(state, {id: 's'});
120
121  const afterGroup = produce(state, draft => {
122    StateActions.addTrackGroup(draft, {
123      engineId: '1',
124      name: 'A track group',
125      id: '123-123-123',
126      summaryTrackId: 's',
127      collapsed: false,
128    });
129  });
130
131  const afterTrackAdd = produce(afterGroup, draft => {
132    StateActions.addTrack(draft, {
133      id: '1',
134      engineId: '1',
135      kind: 'slices',
136      name: 'renderer 1',
137      trackKindPriority: TrackKindPriority.ORDINARY,
138      trackGroup: '123-123-123',
139      config: {},
140    });
141  });
142
143  expect(afterTrackAdd.trackGroups['123-123-123'].tracks[0]).toBe('s');
144  expect(afterTrackAdd.trackGroups['123-123-123'].tracks[1]).toBe('1');
145});
146
147test('reorder tracks', () => {
148  const once = produce(createEmptyState(), draft => {
149    StateActions.addTrack(draft, {
150      engineId: '1',
151      kind: 'cpu',
152      name: 'Cpu 1',
153      trackKindPriority: TrackKindPriority.ORDINARY,
154      config: {},
155    });
156    StateActions.addTrack(draft, {
157      engineId: '2',
158      kind: 'cpu',
159      name: 'Cpu 2',
160      trackKindPriority: TrackKindPriority.ORDINARY,
161      config: {},
162    });
163  });
164
165  const firstTrackId = once.scrollingTracks[0];
166  const secondTrackId = once.scrollingTracks[1];
167
168  const twice = produce(once, draft => {
169    StateActions.moveTrack(draft, {
170      srcId: `${firstTrackId}`,
171      op: 'after',
172      dstId: `${secondTrackId}`,
173    });
174  });
175
176  expect(twice.scrollingTracks[0]).toBe(secondTrackId);
177  expect(twice.scrollingTracks[1]).toBe(firstTrackId);
178});
179
180test('reorder pinned to scrolling', () => {
181  let state = createEmptyState();
182  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
183
184  const after = produce(state, draft => {
185    StateActions.moveTrack(draft, {
186      srcId: 'b',
187      op: 'before',
188      dstId: 'c',
189    });
190  });
191
192  expect(after.pinnedTracks).toEqual(['a']);
193  expect(after.scrollingTracks).toEqual(['b', 'c']);
194});
195
196test('reorder scrolling to pinned', () => {
197  let state = createEmptyState();
198  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
199
200  const after = produce(state, draft => {
201    StateActions.moveTrack(draft, {
202      srcId: 'b',
203      op: 'after',
204      dstId: 'a',
205    });
206  });
207
208  expect(after.pinnedTracks).toEqual(['a', 'b']);
209  expect(after.scrollingTracks).toEqual(['c']);
210});
211
212test('reorder clamp bottom', () => {
213  let state = createEmptyState();
214  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
215
216  const after = produce(state, draft => {
217    StateActions.moveTrack(draft, {
218      srcId: 'a',
219      op: 'before',
220      dstId: 'a',
221    });
222  });
223  expect(after).toEqual(state);
224});
225
226test('reorder clamp top', () => {
227  let state = createEmptyState();
228  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
229
230  const after = produce(state, draft => {
231    StateActions.moveTrack(draft, {
232      srcId: 'c',
233      op: 'after',
234      dstId: 'c',
235    });
236  });
237  expect(after).toEqual(state);
238});
239
240test('pin', () => {
241  let state = createEmptyState();
242  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
243
244  const after = produce(state, draft => {
245    StateActions.toggleTrackPinned(draft, {
246      trackId: 'c',
247    });
248  });
249  expect(after.pinnedTracks).toEqual(['a', 'c']);
250  expect(after.scrollingTracks).toEqual(['b']);
251});
252
253test('unpin', () => {
254  let state = createEmptyState();
255  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
256
257  const after = produce(state, draft => {
258    StateActions.toggleTrackPinned(draft, {
259      trackId: 'a',
260    });
261  });
262  expect(after.pinnedTracks).toEqual(['b']);
263  expect(after.scrollingTracks).toEqual(['a', 'c']);
264});
265
266test('open trace', () => {
267  const state = createEmptyState();
268  state.nextId = 100;
269  const recordConfig = state.recordConfig;
270  const after = produce(state, draft => {
271    StateActions.openTraceFromUrl(draft, {
272      url: 'https://example.com/bar',
273    });
274  });
275
276  const engineKeys = Object.keys(after.engines);
277  expect(after.nextId).toBe(101);
278  expect(engineKeys.length).toBe(1);
279  expect((after.engines[engineKeys[0]].source as TraceUrlSource).url)
280      .toBe('https://example.com/bar');
281  expect(after.route).toBe('/viewer');
282  expect(after.recordConfig).toBe(recordConfig);
283});
284
285test('open second trace from file', () => {
286  const once = produce(createEmptyState(), draft => {
287    StateActions.openTraceFromUrl(draft, {
288      url: 'https://example.com/bar',
289    });
290  });
291
292  const twice = produce(once, draft => {
293    StateActions.addTrack(draft, {
294      engineId: '1',
295      kind: 'cpu',
296      name: 'Cpu 1',
297      trackKindPriority: TrackKindPriority.ORDINARY,
298      config: {},
299    });
300  });
301
302  const thrice = produce(twice, draft => {
303    StateActions.openTraceFromUrl(draft, {
304      url: 'https://example.com/foo',
305    });
306  });
307
308  const engineKeys = Object.keys(thrice.engines);
309  expect(engineKeys.length).toBe(1);
310  expect((thrice.engines[engineKeys[0]].source as TraceUrlSource).url)
311      .toBe('https://example.com/foo');
312  expect(thrice.pinnedTracks.length).toBe(0);
313  expect(thrice.scrollingTracks.length).toBe(0);
314  expect(thrice.route).toBe('/viewer');
315});
316
317test('setEngineReady with missing engine is ignored', () => {
318  const state = createEmptyState();
319  produce(state, draft => {
320    StateActions.setEngineReady(
321        draft, {engineId: '1', ready: true, mode: 'WASM'});
322  });
323});
324
325test('setEngineReady', () => {
326  const state = createEmptyState();
327  state.nextId = 100;
328  const after = produce(state, draft => {
329    StateActions.openTraceFromUrl(draft, {
330      url: 'https://example.com/bar',
331    });
332    StateActions.setEngineReady(
333        draft, {engineId: '100', ready: true, mode: 'WASM'});
334  });
335  expect(after.engines['100'].ready).toBe(true);
336});
337
338test('sortTracksByPriority', () => {
339  let state = createEmptyState();
340  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
341  state = fakeTrack(
342      state, {id: 'b', kind: HEAP_PROFILE_TRACK_KIND, trackGroup: 'g'});
343  state = fakeTrack(
344      state, {id: 'a', kind: PROCESS_SCHEDULING_TRACK_KIND, trackGroup: 'g'});
345
346  const after = produce(state, draft => {
347    StateActions.sortThreadTracks(draft, {});
348  });
349
350  // High Priority tracks should be sorted before Low Priority tracks:
351  // 'b' appears twice because it's the summary track
352  expect(after.trackGroups['g'].tracks).toEqual(['a', 'b', 'b']);
353});
354
355test('sortTracksByPriorityAndKindAndName', () => {
356  let state = createEmptyState();
357  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
358  state = fakeTrack(
359      state, {id: 'a', kind: PROCESS_SCHEDULING_TRACK_KIND, trackGroup: 'g'});
360  state = fakeTrack(state, {
361    id: 'b',
362    kind: SLICE_TRACK_KIND,
363    trackGroup: 'g',
364    trackKindPriority: TrackKindPriority.MAIN_THREAD
365  });
366  state = fakeTrack(state, {
367    id: 'c',
368    kind: SLICE_TRACK_KIND,
369    trackGroup: 'g',
370    trackKindPriority: TrackKindPriority.RENDER_THREAD
371  });
372  state = fakeTrack(state, {
373    id: 'd',
374    kind: SLICE_TRACK_KIND,
375    trackGroup: 'g',
376    trackKindPriority: TrackKindPriority.GPU_COMPLETION
377  });
378  state = fakeTrack(
379      state, {id: 'e', kind: HEAP_PROFILE_TRACK_KIND, trackGroup: 'g'});
380  state = fakeTrack(
381      state, {id: 'f', kind: SLICE_TRACK_KIND, trackGroup: 'g', name: 'T2'});
382  state = fakeTrack(
383      state, {id: 'g', kind: SLICE_TRACK_KIND, trackGroup: 'g', name: 'T10'});
384
385  const after = produce(state, draft => {
386    StateActions.sortThreadTracks(draft, {});
387  });
388
389  // The order should be determined by:
390  // 1.High priority
391  // 2.Non ordinary track kinds
392  // 3.Low priority
393  // 4.Collated name string (ie. 'T2' will be before 'T10')
394  expect(after.trackGroups['g'].tracks)
395      .toEqual(['a', 'b', 'b', 'c', 'd', 'e', 'f', 'g']);
396});
397
398test('sortTracksByTidThenName', () => {
399  let state = createEmptyState();
400  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'a'});
401  state = fakeTrack(state, {
402    id: 'a',
403    kind: SLICE_TRACK_KIND,
404    trackGroup: 'g',
405    name: 'aaa',
406    tid: '1'
407  });
408  state = fakeTrack(state, {
409    id: 'b',
410    kind: SLICE_TRACK_KIND,
411    trackGroup: 'g',
412    name: 'bbb',
413    tid: '2'
414  });
415  state = fakeTrack(state, {
416    id: 'c',
417    kind: THREAD_STATE_TRACK_KIND,
418    trackGroup: 'g',
419    name: 'ccc',
420    tid: '1'
421  });
422
423  const after = produce(state, draft => {
424    StateActions.sortThreadTracks(draft, {});
425  });
426
427  expect(after.trackGroups['g'].tracks).toEqual(['a', 'a', 'c', 'b']);
428});
429