UNPKG

chrome-devtools-frontend

Version:
468 lines (417 loc) • 19.6 kB
// Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as Platform from '../../core/platform/platform.js'; import * as Trace from '../../models/trace/trace.js'; import * as TimelineComponents from '../../panels/timeline/components/components.js'; import * as AnnotationHelpers from './AnnotationHelpers.js'; import {EntriesFilter} from './EntriesFilter.js'; import {EventsSerializer} from './EventsSerializer.js'; import type * as Overlays from './overlays/overlays.js'; const modificationsManagerByTraceIndex: ModificationsManager[] = []; let activeManager: ModificationsManager|null; export type UpdateAction = 'Remove'|'Add'|'UpdateLabel'|'UpdateTimeRange'|'UpdateLinkToEntry'|'EnterLabelEditState'; // Event dispatched after an annotation was added, removed or updated. // The event argument is the Overlay that needs to be created,removed // or updated by `Overlays.ts` and the action that needs to be applied to it. export class AnnotationModifiedEvent extends Event { static readonly eventName = 'annotationmodifiedevent'; constructor(public overlay: Overlays.Overlays.TimelineOverlay, public action: UpdateAction) { super(AnnotationModifiedEvent.eventName); } } interface ModificationsManagerData { parsedTrace: Trace.Handlers.Types.ParsedTrace; traceBounds: Trace.Types.Timing.TraceWindowMicro; rawTraceEvents: readonly Trace.Types.Events.Event[]; syntheticEvents: Trace.Types.Events.SyntheticBased[]; modifications?: Trace.Types.File.Modifications; } export class ModificationsManager extends EventTarget { #entriesFilter: EntriesFilter; #timelineBreadcrumbs: TimelineComponents.Breadcrumbs.Breadcrumbs; #modifications: Trace.Types.File.Modifications|null = null; #parsedTrace: Trace.Handlers.Types.ParsedTrace; #eventsSerializer: EventsSerializer; #overlayForAnnotation: Map<Trace.Types.File.Annotation, Overlays.Overlays.TimelineOverlay>; readonly #annotationsHiddenSetting: Common.Settings.Setting<boolean>; /** * Gets the ModificationsManager instance corresponding to a trace * given its index used in Model#traces. If no index is passed gets * the manager instance for the last trace. If no instance is found, * throws. */ static activeManager(): ModificationsManager|null { return activeManager; } static reset(): void { modificationsManagerByTraceIndex.length = 0; activeManager = null; } /** * Initializes a ModificationsManager instance for a parsed trace or changes the active manager for an existing one. * This needs to be called if and a trace has been parsed or switched to. */ static initAndActivateModificationsManager(traceModel: Trace.TraceModel.Model, traceIndex: number): ModificationsManager|null { // If a manager for a given index has already been created, active it. if (modificationsManagerByTraceIndex[traceIndex]) { if (activeManager === modificationsManagerByTraceIndex[traceIndex]) { return activeManager; } activeManager = modificationsManagerByTraceIndex[traceIndex]; ModificationsManager.activeManager()?.applyModificationsIfPresent(); } const parsedTrace = traceModel.parsedTrace(traceIndex); if (!parsedTrace) { throw new Error('ModificationsManager was initialized without a corresponding trace data'); } const traceBounds = parsedTrace.Meta.traceBounds; const traceEvents = traceModel.rawTraceEvents(traceIndex); if (!traceEvents) { throw new Error('ModificationsManager was initialized without a corresponding raw trace events array'); } const syntheticEventsManager = traceModel.syntheticTraceEventsManager(traceIndex); if (!syntheticEventsManager) { throw new Error('ModificationsManager was initialized without a corresponding SyntheticEventsManager'); } const metadata = traceModel.metadata(traceIndex); const newModificationsManager = new ModificationsManager({ parsedTrace, traceBounds, rawTraceEvents: traceEvents, modifications: metadata?.modifications, syntheticEvents: syntheticEventsManager.getSyntheticTraces(), }); modificationsManagerByTraceIndex[traceIndex] = newModificationsManager; activeManager = newModificationsManager; ModificationsManager.activeManager()?.applyModificationsIfPresent(); return this.activeManager(); } private constructor({parsedTrace, traceBounds, modifications}: ModificationsManagerData) { super(); this.#entriesFilter = new EntriesFilter(parsedTrace); // Create first breadcrumb from the initial full window this.#timelineBreadcrumbs = new TimelineComponents.Breadcrumbs.Breadcrumbs(traceBounds); this.#modifications = modifications || null; this.#parsedTrace = parsedTrace; this.#eventsSerializer = new EventsSerializer(); // This method is also called in SidebarAnnotationsTab, but calling this multiple times doesn't recreate the setting. // Instead, after the second call, the cached setting is returned. this.#annotationsHiddenSetting = Common.Settings.Settings.instance().moduleSetting('annotations-hidden'); // TODO: Assign annotations loaded from the trace file this.#overlayForAnnotation = new Map(); } getEntriesFilter(): EntriesFilter { return this.#entriesFilter; } getTimelineBreadcrumbs(): TimelineComponents.Breadcrumbs.Breadcrumbs { return this.#timelineBreadcrumbs; } deleteEmptyRangeAnnotations(): void { for (const annotation of this.#overlayForAnnotation.keys()) { if (annotation.type === 'TIME_RANGE' && annotation.label.length === 0) { this.removeAnnotation(annotation); } } } /** * Stores the annotation and creates its overlay. * @returns the Overlay that gets created and associated with this annotation. */ createAnnotation(newAnnotation: Trace.Types.File.Annotation, loadedFromFile = false): Overlays.Overlays.TimelineOverlay { // If a label already exists on an entry and a user is trying to create a new one, start editing an existing label instead. if (newAnnotation.type === 'ENTRY_LABEL') { const overlay = this.#findLabelOverlayForEntry(newAnnotation.entry); if (overlay) { this.dispatchEvent(new AnnotationModifiedEvent(overlay, 'EnterLabelEditState')); return overlay; } } // If the new annotation created was not loaded from the file, set the annotations visibility setting to true. That way we make sure // the annotations are on when a new one is created. if (!loadedFromFile) { // Time range annotation could also be used to check the length of a selection in the timeline. Therefore, only set the annotations // hidden to true if annotations label is added. This is done in OverlaysImpl. if (newAnnotation.type !== 'TIME_RANGE') { this.#annotationsHiddenSetting.set(false); } } const newOverlay = this.#createOverlayFromAnnotation(newAnnotation); this.#overlayForAnnotation.set(newAnnotation, newOverlay); this.dispatchEvent(new AnnotationModifiedEvent(newOverlay, 'Add')); return newOverlay; } annotationsForEntry(entry: Trace.Types.Events.Event): Trace.Types.File.Annotation[] { const annotationsForEntry = []; for (const [annotation] of this.#overlayForAnnotation.entries()) { if (annotation.type === 'ENTRY_LABEL' && annotation.entry === entry) { annotationsForEntry.push(annotation); } else if ( annotation.type === 'ENTRIES_LINK' && (annotation.entryFrom === entry || annotation.entryTo === entry)) { annotationsForEntry.push(annotation); } } return annotationsForEntry; } // Deletes all annotations associated with an entry deleteEntryAnnotations(entry: Trace.Types.Events.Event): void { const annotationsForEntry = this.annotationsForEntry(entry); annotationsForEntry.forEach(annotation => { this.removeAnnotation(annotation); }); } linkAnnotationBetweenEntriesExists(entryFrom: Trace.Types.Events.Event, entryTo: Trace.Types.Events.Event): boolean { for (const annotation of this.#overlayForAnnotation.keys()) { if (annotation.type === 'ENTRIES_LINK' && ((annotation.entryFrom === entryFrom && annotation.entryTo === entryTo) || (annotation.entryFrom === entryTo && annotation.entryTo === entryFrom))) { return true; } } return false; } #findLabelOverlayForEntry(entry: Trace.Types.Events.Event): Overlays.Overlays.TimelineOverlay|null { for (const [annotation, overlay] of this.#overlayForAnnotation.entries()) { if (annotation.type === 'ENTRY_LABEL' && annotation.entry === entry) { return overlay; } } return null; } #createOverlayFromAnnotation(annotation: Trace.Types.File.Annotation): Overlays.Overlays.EntryLabel |Overlays.Overlays.TimeRangeLabel|Overlays.Overlays.EntriesLink { switch (annotation.type) { case 'ENTRY_LABEL': return { type: 'ENTRY_LABEL', entry: annotation.entry, label: annotation.label, }; case 'TIME_RANGE': return { type: 'TIME_RANGE', label: annotation.label, showDuration: true, bounds: annotation.bounds, }; case 'ENTRIES_LINK': return { type: 'ENTRIES_LINK', state: annotation.state, entryFrom: annotation.entryFrom, entryTo: annotation.entryTo, }; default: Platform.assertNever(annotation, 'Overlay for provided annotation cannot be created'); } } removeAnnotation(removedAnnotation: Trace.Types.File.Annotation): void { const overlayToRemove = this.#overlayForAnnotation.get(removedAnnotation); if (!overlayToRemove) { console.warn('Overlay for deleted Annotation does not exist', removedAnnotation); return; } this.#overlayForAnnotation.delete(removedAnnotation); this.dispatchEvent(new AnnotationModifiedEvent(overlayToRemove, 'Remove')); } removeAnnotationOverlay(removedOverlay: Overlays.Overlays.TimelineOverlay): void { const annotationForRemovedOverlay = this.getAnnotationByOverlay(removedOverlay); if (!annotationForRemovedOverlay) { console.warn('Annotation for deleted Overlay does not exist', removedOverlay); return; } this.removeAnnotation(annotationForRemovedOverlay); } updateAnnotation(updatedAnnotation: Trace.Types.File.Annotation): void { const overlay = this.#overlayForAnnotation.get(updatedAnnotation); if (overlay && AnnotationHelpers.isTimeRangeLabel(overlay) && Trace.Types.File.isTimeRangeAnnotation(updatedAnnotation)) { overlay.label = updatedAnnotation.label; overlay.bounds = updatedAnnotation.bounds; this.dispatchEvent(new AnnotationModifiedEvent(overlay, 'UpdateTimeRange')); } else if ( overlay && AnnotationHelpers.isEntriesLink(overlay) && Trace.Types.File.isEntriesLinkAnnotation(updatedAnnotation)) { overlay.state = updatedAnnotation.state; overlay.entryFrom = updatedAnnotation.entryFrom; overlay.entryTo = updatedAnnotation.entryTo; this.dispatchEvent(new AnnotationModifiedEvent(overlay, 'UpdateLinkToEntry')); } else { console.error('Annotation could not be updated'); } } updateAnnotationOverlay(updatedOverlay: Overlays.Overlays.TimelineOverlay): void { const annotationForUpdatedOverlay = this.getAnnotationByOverlay(updatedOverlay); if (!annotationForUpdatedOverlay) { console.warn('Annotation for updated Overlay does not exist'); return; } if ((updatedOverlay.type === 'ENTRY_LABEL' && annotationForUpdatedOverlay.type === 'ENTRY_LABEL') || (updatedOverlay.type === 'TIME_RANGE' && annotationForUpdatedOverlay.type === 'TIME_RANGE')) { this.#annotationsHiddenSetting.set(false); annotationForUpdatedOverlay.label = updatedOverlay.label; this.dispatchEvent(new AnnotationModifiedEvent(updatedOverlay, 'UpdateLabel')); } if ((updatedOverlay.type === 'ENTRIES_LINK' && annotationForUpdatedOverlay.type === 'ENTRIES_LINK')) { this.#annotationsHiddenSetting.set(false); annotationForUpdatedOverlay.state = updatedOverlay.state; } } getAnnotationByOverlay(overlay: Overlays.Overlays.TimelineOverlay): Trace.Types.File.Annotation|null { for (const [annotation, currOverlay] of this.#overlayForAnnotation.entries()) { if (currOverlay === overlay) { return annotation; } } return null; } getAnnotations(): Trace.Types.File.Annotation[] { return [...this.#overlayForAnnotation.keys()]; } getOverlays(): Overlays.Overlays.TimelineOverlay[] { return [...this.#overlayForAnnotation.values()]; } applyAnnotationsFromCache(): void { this.#modifications = this.toJSON(); // The cache is filled by applyModificationsIfPresent, so we clear // it beforehand to prevent duplicate entries. this.#overlayForAnnotation.clear(); this.#applyStoredAnnotations(this.#modifications.annotations); } /** * Builds all modifications into a serializable object written into * the 'modifications' trace file metadata field. */ toJSON(): Trace.Types.File.Modifications { const hiddenEntries = this.#entriesFilter.invisibleEntries() .map(entry => this.#eventsSerializer.keyForEvent(entry)) .filter(entry => entry !== null); const expandableEntries = this.#entriesFilter.expandableEntries() .map(entry => this.#eventsSerializer.keyForEvent(entry)) .filter(entry => entry !== null); this.#modifications = { entriesModifications: { hiddenEntries, expandableEntries, }, initialBreadcrumb: this.#timelineBreadcrumbs.initialBreadcrumb, annotations: this.#annotationsJSON(), }; return this.#modifications; } #annotationsJSON(): Trace.Types.File.SerializedAnnotations { const annotations = this.getAnnotations(); const entryLabelsSerialized: Trace.Types.File.EntryLabelAnnotationSerialized[] = []; const labelledTimeRangesSerialized: Trace.Types.File.TimeRangeAnnotationSerialized[] = []; const linksBetweenEntriesSerialized: Trace.Types.File.EntriesLinkAnnotationSerialized[] = []; for (let i = 0; i < annotations.length; i++) { const currAnnotation = annotations[i]; if (Trace.Types.File.isEntryLabelAnnotation(currAnnotation)) { const serializedEvent = this.#eventsSerializer.keyForEvent(currAnnotation.entry); if (serializedEvent) { entryLabelsSerialized.push({ entry: serializedEvent, label: currAnnotation.label, }); } } else if (Trace.Types.File.isTimeRangeAnnotation(currAnnotation)) { labelledTimeRangesSerialized.push({ bounds: currAnnotation.bounds, label: currAnnotation.label, }); } else if (Trace.Types.File.isEntriesLinkAnnotation(currAnnotation)) { // Only save the links between entries that are fully created and have the entry that it is pointing to set if (currAnnotation.entryTo) { const serializedFromEvent = this.#eventsSerializer.keyForEvent(currAnnotation.entryFrom); const serializedToEvent = this.#eventsSerializer.keyForEvent(currAnnotation.entryTo); if (serializedFromEvent && serializedToEvent) { linksBetweenEntriesSerialized.push({ entryFrom: serializedFromEvent, entryTo: serializedToEvent, }); } } } } return { entryLabels: entryLabelsSerialized, labelledTimeRanges: labelledTimeRangesSerialized, linksBetweenEntries: linksBetweenEntriesSerialized, }; } applyModificationsIfPresent(): void { if (!this.#modifications || !this.#modifications.annotations) { return; } const hiddenEntries = this.#modifications.entriesModifications.hiddenEntries; const expandableEntries = this.#modifications.entriesModifications.expandableEntries; this.#timelineBreadcrumbs.setInitialBreadcrumbFromLoadedModifications(this.#modifications.initialBreadcrumb); this.#applyEntriesFilterModifications(hiddenEntries, expandableEntries); this.#applyStoredAnnotations(this.#modifications.annotations); } #applyStoredAnnotations(annotations: Trace.Types.File.SerializedAnnotations): void { try { // Assign annotations to an empty array if they don't exist to not // break the traces that were saved before those annotations were implemented const entryLabels = annotations.entryLabels ?? []; entryLabels.forEach(entryLabel => { this.createAnnotation( { type: 'ENTRY_LABEL', entry: this.#eventsSerializer.eventForKey(entryLabel.entry, this.#parsedTrace), label: entryLabel.label, }, true); }); const timeRanges = annotations.labelledTimeRanges ?? []; timeRanges.forEach(timeRange => { this.createAnnotation( { type: 'TIME_RANGE', bounds: timeRange.bounds, label: timeRange.label, }, true); }); const linksBetweenEntries = annotations.linksBetweenEntries ?? []; linksBetweenEntries.forEach(linkBetweenEntries => { this.createAnnotation( { type: 'ENTRIES_LINK', state: Trace.Types.File.EntriesLinkState.CONNECTED, entryFrom: this.#eventsSerializer.eventForKey(linkBetweenEntries.entryFrom, this.#parsedTrace), entryTo: this.#eventsSerializer.eventForKey(linkBetweenEntries.entryTo, this.#parsedTrace), }, true); }); } catch (err) { // This function is wrapped in a try/catch just in case we get any incoming // trace files with broken event keys. Shouldn't happen of course, but if // it does, we can discard all the data and then continue loading the // trace, rather than have the panel entirely break. This also prevents any // issue where we accidentally break the event serializer and break people // loading traces; let's at least make sure they can load the panel, even // if their annotations are gone. console.warn('Failed to apply stored annotations', err); } } #applyEntriesFilterModifications( hiddenEntriesKeys: Trace.Types.File.SerializableKey[], expandableEntriesKeys: Trace.Types.File.SerializableKey[]): void { try { const hiddenEntries = hiddenEntriesKeys.map(key => this.#eventsSerializer.eventForKey(key, this.#parsedTrace)); const expandableEntries = expandableEntriesKeys.map(key => this.#eventsSerializer.eventForKey(key, this.#parsedTrace)); this.#entriesFilter.setHiddenAndExpandableEntries(hiddenEntries, expandableEntries); } catch (err) { console.warn('Failed to apply entriesFilter modifications', err); // If there was some invalid data, let's just back out and clear it // entirely. This is better than applying a subset of all the hidden // entries, which could cause an odd state in the flamechart. this.#entriesFilter.setHiddenAndExpandableEntries([], []); } } }