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