xref: /aosp_15_r20/external/perfetto/ui/src/core/state_serialization.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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