1// Copyright (C) 2024 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 { 16 SERIALIZED_STATE_VERSION, 17 APP_STATE_SCHEMA, 18 SerializedNote, 19 SerializedPluginState, 20 SerializedSelection, 21 SerializedAppState, 22} from './state_serialization_schema'; 23import {TimeSpan} from '../base/time'; 24import {TraceImpl} from './trace_impl'; 25 26// When it comes to serialization & permalinks there are two different use cases 27// 1. Uploading the current trace in a Cloud Storage (GCS) file AND serializing 28// the app state into a different GCS JSON file. This is what happens when 29// clicking on "share trace" on a local file manually opened. 30// 2. [future use case] Uploading the current state in a GCS JSON file, but 31// letting the trace file come from a deep-link via postMessage(). 32// This is the case when traces are opened via Dashboards (e.g. APC) and we 33// want to persist only the state itself, not the trace file. 34// 35// In order to do so, we have two layers of serialization 36// 1. Serialization of the app state (This file): 37// This is a JSON object that represents the visual app state (pinned tracks, 38// visible viewport bounds, etc) BUT not the trace source. 39// 2. An outer layer that contains the app state AND a link to the trace file. 40// (permalink.ts) 41// 42// In a nutshell: 43// AppState: {viewport: {...}, pinnedTracks: {...}, notes: {...}} 44// Permalink: {appState: {see above}, traceUrl: 'https://gcs/trace/file'} 45// 46// This file deals with the app state. permalink.ts deals with the outer layer. 47 48/** 49 * Serializes the current app state into a JSON-friendly POJO that can be stored 50 * in a permalink (@see permalink.ts). 51 * @returns A @type {SerializedAppState} object, @see state_serialization_schema.ts 52 */ 53export function serializeAppState(trace: TraceImpl): SerializedAppState { 54 const vizWindow = trace.timeline.visibleWindow.toTimeSpan(); 55 56 const notes = new Array<SerializedNote>(); 57 for (const [id, note] of trace.notes.notes.entries()) { 58 if (note.noteType === 'DEFAULT') { 59 notes.push({ 60 noteType: 'DEFAULT', 61 id, 62 start: note.timestamp, 63 color: note.color, 64 text: note.text, 65 }); 66 } else if (note.noteType === 'SPAN') { 67 notes.push({ 68 noteType: 'SPAN', 69 id, 70 start: note.start, 71 end: note.end, 72 color: note.color, 73 text: note.text, 74 }); 75 } 76 } 77 78 const selection = new Array<SerializedSelection>(); 79 const stateSel = trace.selection.selection; 80 if (stateSel.kind === 'track_event') { 81 selection.push({ 82 kind: 'TRACK_EVENT', 83 trackKey: stateSel.trackUri, 84 eventId: stateSel.eventId.toString(), 85 detailsPanel: trace.selection 86 .getDetailsPanelForSelection() 87 ?.serializatonState(), 88 }); 89 } else if (stateSel.kind === 'area') { 90 selection.push({ 91 kind: 'AREA', 92 trackUris: stateSel.trackUris, 93 start: stateSel.start, 94 end: stateSel.end, 95 }); 96 } 97 98 const plugins = new Array<SerializedPluginState>(); 99 const pluginsStore = trace.getPluginStoreForSerialization(); 100 101 for (const [id, pluginState] of Object.entries(pluginsStore)) { 102 plugins.push({id, state: pluginState}); 103 } 104 105 return { 106 version: SERIALIZED_STATE_VERSION, 107 pinnedTracks: trace.workspace.pinnedTracks 108 .map((t) => t.uri) 109 .filter((uri) => uri !== undefined), 110 viewport: { 111 start: vizWindow.start, 112 end: vizWindow.end, 113 }, 114 notes, 115 selection, 116 plugins, 117 }; 118} 119 120export type ParseStateResult = 121 | {success: true; data: SerializedAppState} 122 | {success: false; error: string}; 123 124/** 125 * Parses the app state from a JSON blob. 126 * @param jsonDecodedObj the output of JSON.parse() that needs validation 127 * @returns Either a @type {SerializedAppState} object or an error. 128 */ 129export function parseAppState(jsonDecodedObj: unknown): ParseStateResult { 130 const parseRes = APP_STATE_SCHEMA.safeParse(jsonDecodedObj); 131 if (parseRes.success) { 132 if (parseRes.data.version == SERIALIZED_STATE_VERSION) { 133 return {success: true, data: parseRes.data}; 134 } else { 135 return { 136 success: false, 137 error: 138 `SERIALIZED_STATE_VERSION mismatch ` + 139 `(actual: ${parseRes.data.version}, ` + 140 `expected: ${SERIALIZED_STATE_VERSION})`, 141 }; 142 } 143 } 144 return {success: false, error: parseRes.error.toString()}; 145} 146 147/** 148 * This function gets invoked after the trace is loaded, but before plugins, 149 * track decider and initial selections are run. 150 * @param appState the .data object returned by parseAppState() when successful. 151 */ 152export function deserializeAppStatePhase1( 153 appState: SerializedAppState, 154 trace: TraceImpl, 155): void { 156 // Restore the plugin state. 157 trace.getPluginStoreForSerialization().edit((draft) => { 158 for (const p of appState.plugins ?? []) { 159 draft[p.id] = p.state ?? {}; 160 } 161 }); 162} 163 164/** 165 * This function gets invoked after the trace controller has run and all plugins 166 * have executed. 167 * @param appState the .data object returned by parseAppState() when successful. 168 * @param trace the target trace object to manipulate. 169 */ 170export function deserializeAppStatePhase2( 171 appState: SerializedAppState, 172 trace: TraceImpl, 173): void { 174 if (appState.viewport !== undefined) { 175 trace.timeline.updateVisibleTime( 176 new TimeSpan(appState.viewport.start, appState.viewport.end), 177 ); 178 } 179 180 // Restore the pinned tracks, if they exist. 181 for (const uri of appState.pinnedTracks) { 182 const track = trace.workspace.findTrackByUri(uri); 183 if (track) { 184 track.pin(); 185 } 186 } 187 188 // Restore notes. 189 for (const note of appState.notes) { 190 const commonArgs = { 191 id: note.id, 192 timestamp: note.start, 193 color: note.color, 194 text: note.text, 195 }; 196 if (note.noteType === 'DEFAULT') { 197 trace.notes.addNote({...commonArgs}); 198 } else if (note.noteType === 'SPAN') { 199 trace.notes.addSpanNote({ 200 ...commonArgs, 201 start: commonArgs.timestamp, 202 end: note.end, 203 }); 204 } 205 } 206 207 // Restore the selection 208 trace.selection.deserialize(appState.selection[0]); 209} 210 211/** 212 * Performs JSON serialization, taking care of also serializing BigInt->string. 213 * For the matching deserializer see zType in state_serialization_schema.ts. 214 * @param obj A POJO, typically a SerializedAppState or PermalinkState. 215 * @returns JSON-encoded string. 216 */ 217export function JsonSerialize(obj: Object): string { 218 return JSON.stringify(obj, (_key, value) => { 219 if (typeof value === 'bigint') { 220 return value.toString(); 221 } 222 return value; 223 }); 224} 225