diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index a8a752bc3fe..d01e696fd1f 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni @@ -1941,9 +1941,12 @@ grd_files_debug_sources = [ "front_end/panels/timeline/components/NetworkThrottlingSelector.js", "front_end/panels/timeline/components/OriginMap.js", "front_end/panels/timeline/components/RelatedInsightChips.js", + "front_end/panels/timeline/components/RNPerfIssueTypes.js", "front_end/panels/timeline/components/Sidebar.js", "front_end/panels/timeline/components/SidebarAnnotationsTab.js", "front_end/panels/timeline/components/SidebarInsightsTab.js", + "front_end/panels/timeline/components/SidebarRNPerfSignalsTab.js", + "front_end/panels/timeline/components/SidebarRNPerfIssueItem.js", "front_end/panels/timeline/components/SidebarSingleInsightSet.js", "front_end/panels/timeline/components/TimelineSummary.js", "front_end/panels/timeline/components/Utils.js", @@ -1994,6 +1997,8 @@ grd_files_debug_sources = [ "front_end/panels/timeline/components/relatedInsightChips.css.js", "front_end/panels/timeline/components/sidebarAnnotationsTab.css.js", "front_end/panels/timeline/components/sidebarInsightsTab.css.js", + "front_end/panels/timeline/components/sidebarRNPerfIssuesTab.css.js", + "front_end/panels/timeline/components/sidebarRNPerfIssueItem.css.js", "front_end/panels/timeline/components/sidebarSingleInsightSet.css.js", "front_end/panels/timeline/components/timelineSummary.css.js", "front_end/panels/timeline/extensions/ExtensionUI.js", diff --git a/front_end/panels/timeline/TimelineFlameChartView.ts b/front_end/panels/timeline/TimelineFlameChartView.ts index 3bf8cc3edb7..b4dddd8490b 100644 --- a/front_end/panels/timeline/TimelineFlameChartView.ts +++ b/front_end/panels/timeline/TimelineFlameChartView.ts @@ -178,6 +178,7 @@ export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin { + dimmer.active = false; + }); + + const mainIndex = this.mainDataProvider.indexForEvent(event); + const networkIndex = this.networkDataProvider.indexForEvent(event); + const mainChartIndices = mainIndex !== null && mainIndex >= 0 ? [mainIndex] : []; + const networkChartIndices = networkIndex !== null && networkIndex >= 0 ? [networkIndex] : []; + + this.#updateFlameChartDimmerWithIndices(this.#performanceIssuesDimmer, mainChartIndices, networkChartIndices); + } + + #clearPerformanceIssuesDimmer(): void { + this.#updateFlameChartDimmerWithEvents(this.#performanceIssuesDimmer, null); + } + #dimInsightRelatedEvents(relatedEvents: Trace.Types.Events.Event[]): void { // Dim all events except those related to the active insight. const relatedMainIndices = relatedEvents.map(event => this.mainDataProvider.indexForEvent(event) ?? -1); @@ -1595,6 +1616,9 @@ export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin { + const selection = selectionFromEvent(event); + this.flameChart.setSelectionAndReveal(selection); + this.flameChart.updatePerfIssueFlameChartDimmer(event); + }); + this.flameChart.overlays().addEventListener(Overlays.Overlays.TimeRangeMouseOverEvent.eventName, event => { const {overlay} = event as Overlays.Overlays.TimeRangeMouseOverEvent; const overlayBounds = Overlays.Overlays.traceWindowContainingOverlays([overlay]); @@ -2185,10 +2192,6 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod // Used in interaction tests & screenshot tests. return; } - // [RN] Keep sidebar collapsed by default - if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI)) { - return; - } const needToRestore = this.#restoreSidebarVisibilityOnTraceLoad; const userHasSeenSidebar = this.#sideBar.userHasOpenedSidebarOnce(); diff --git a/front_end/panels/timeline/components/BUILD.gn b/front_end/panels/timeline/components/BUILD.gn index 7ff2bc167c3..0db0a5b6eb6 100644 --- a/front_end/panels/timeline/components/BUILD.gn +++ b/front_end/panels/timeline/components/BUILD.gn @@ -25,6 +25,8 @@ generate_css("css_files") { "relatedInsightChips.css", "sidebarAnnotationsTab.css", "sidebarInsightsTab.css", + "sidebarRNPerfIssueItem.css", + "sidebarRNPerfIssuesTab.css", "sidebarSingleInsightSet.css", "timelineSummary.css", ] @@ -48,9 +50,12 @@ devtools_module("components") { "NetworkThrottlingSelector.ts", "OriginMap.ts", "RelatedInsightChips.ts", + "RNPerfIssueTypes.ts", "Sidebar.ts", "SidebarAnnotationsTab.ts", "SidebarInsightsTab.ts", + "SidebarRNPerfIssueItem.ts", + "SidebarRNPerfSignalsTab.ts", "SidebarSingleInsightSet.ts", "TimelineSummary.ts", "Utils.ts", diff --git a/front_end/panels/timeline/components/RNPerfIssueTypes.ts b/front_end/panels/timeline/components/RNPerfIssueTypes.ts new file mode 100644 index 00000000000..75fc84b81cc --- /dev/null +++ b/front_end/panels/timeline/components/RNPerfIssueTypes.ts @@ -0,0 +1,29 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// Copyright 2024 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 type * as Trace from '../../../models/trace/trace.js'; + +export type PerfIssueSeverity = 'info' | 'warning' | 'error'; + +export interface RNPerfIssueDetail { + name: string; + description?: string; + severity?: PerfIssueSeverity; + learnMoreUrl?: string; +} + +export interface PerfIssueEvent { + event: Trace.Types.Events.Event; + timestampMs: number; +} + +export interface AggregatedPerfIssue { + name: string; + description?: string; + severity: PerfIssueSeverity; + learnMoreUrl?: string; + events: PerfIssueEvent[]; + count: number; +} diff --git a/front_end/panels/timeline/components/Sidebar.ts b/front_end/panels/timeline/components/Sidebar.ts index c6b776c5dfa..f36506d0523 100644 --- a/front_end/panels/timeline/components/Sidebar.ts +++ b/front_end/panels/timeline/components/Sidebar.ts @@ -9,6 +9,7 @@ import * as UI from '../../../ui/legacy/legacy.js'; import {SidebarAnnotationsTab} from './SidebarAnnotationsTab.js'; import {SidebarInsightsTab} from './SidebarInsightsTab.js'; +import {SidebarRNPerfSignalsTab} from './SidebarRNPerfSignalsTab.js'; export interface ActiveInsight { model: Trace.Insights.Types.InsightModel; @@ -40,6 +41,7 @@ declare global { export const enum SidebarTabs { INSIGHTS = 'insights', ANNOTATIONS = 'annotations', + PERF_SIGNALS = 'perf-signals', } export const DEFAULT_SIDEBAR_TAB = SidebarTabs.INSIGHTS; @@ -52,6 +54,9 @@ export class SidebarWidget extends UI.Widget.VBox { #insightsView = new InsightsView(); #annotationsView = new AnnotationsView(); + #perfIssuesTabEnabled = false; + #rnPerfSignalsView = new RNPerfSignalsView(); + /** * Track if the user has opened the sidebar before. We do this so that the * very first time they record/import a trace after the sidebar ships, we can @@ -72,6 +77,8 @@ export class SidebarWidget extends UI.Widget.VBox { // [RN] Disable Insights tab const showInsightsTab = !Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI); + // [RN] Show experimental Performance Issues tab + this.#perfIssuesTabEnabled = Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.REACT_NATIVE_SPECIFIC_UI); if (showInsightsTab) { this.#tabbedPane.appendTab( @@ -114,6 +121,25 @@ export class SidebarWidget extends UI.Widget.VBox { setParsedTrace(parsedTrace: Trace.Handlers.Types.ParsedTrace|null, metadata: Trace.Types.File.MetaData|null): void { this.#insightsView.setParsedTrace(parsedTrace, metadata); + this.#rnPerfSignalsView.setParsedTrace(parsedTrace); + + // [RN] Show or remove the Perf Issues tab if there are issues present + if (this.#perfIssuesTabEnabled) { + const hasIssues = this.#rnPerfSignalsView.hasIssues(); + const tabExists = this.#tabbedPane.hasTab(SidebarTabs.PERF_SIGNALS); + + if (hasIssues && !tabExists) { + this.#tabbedPane.appendTab( + SidebarTabs.PERF_SIGNALS, 'Performance Signals', this.#rnPerfSignalsView, undefined, undefined, false, true); + this.#tabbedPane.selectTab(SidebarTabs.PERF_SIGNALS); + } else if (!hasIssues && tabExists) { + this.#tabbedPane.closeTab(SidebarTabs.PERF_SIGNALS); + } + } + } + + setSelectTimelineEventCallback(callback: (event: Trace.Types.Events.Event) => void): void { + this.#rnPerfSignalsView.setSelectTimelineEventCallback(callback); } setInsights(insights: Trace.Insights.Types.TraceInsightSets|null): void { @@ -185,3 +211,26 @@ class AnnotationsView extends UI.Widget.VBox { return this.#component.deduplicatedAnnotations(); } } + +/** [RN] Experimental view for Performance Signals. */ +class RNPerfSignalsView extends UI.Widget.VBox { + #component = new SidebarRNPerfSignalsTab(); + + constructor() { + super(); + this.element.classList.add('sidebar-perf-signals'); + this.element.appendChild(this.#component); + } + + setParsedTrace(parsedTrace: Trace.Handlers.Types.ParsedTrace|null): void { + this.#component.parsedTrace = parsedTrace; + } + + setSelectTimelineEventCallback(callback: (event: Trace.Types.Events.Event) => void): void { + this.#component.setSelectTimelineEventCallback(callback); + } + + hasIssues(): boolean { + return this.#component.hasIssues(); + } +} diff --git a/front_end/panels/timeline/components/SidebarRNPerfIssueItem.ts b/front_end/panels/timeline/components/SidebarRNPerfIssueItem.ts new file mode 100644 index 00000000000..3b04ea7d073 --- /dev/null +++ b/front_end/panels/timeline/components/SidebarRNPerfIssueItem.ts @@ -0,0 +1,136 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// Copyright 2024 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 '../../../ui/components/buttons/buttons.js'; +import '../../../ui/components/icon_button/icon_button.js'; + +import type * as Platform from '../../../core/platform/platform.js'; +import type * as Trace from '../../../models/trace/trace.js'; +import * as Buttons from '../../../ui/components/buttons/buttons.js'; +import * as UI from '../../../ui/legacy/legacy.js'; +import * as Lit from '../../../ui/lit/lit.js'; + +import type {AggregatedPerfIssue, PerfIssueEvent, PerfIssueSeverity} from './RNPerfIssueTypes.js'; +import styles from './sidebarRNPerfIssueItem.css.js'; + +const {html} = Lit; + +interface SidebarRNPerfIssueItemData { + issue: AggregatedPerfIssue; + onEventSelected?: (event: Trace.Types.Events.Event) => void; +} + +export class SidebarRNPerfIssueItem extends HTMLElement { + readonly #shadow = this.attachShadow({mode: 'open'}); + + #issue: AggregatedPerfIssue|null = null; + #onEventSelected?: (event: Trace.Types.Events.Event) => void; + #isOpen = false; + + set data(data: SidebarRNPerfIssueItemData) { + this.#issue = data.issue; + this.#onEventSelected = data.onEventSelected; + this.#render(); + } + + #toggleOpen(event: Event): void { + event.preventDefault(); + this.#isOpen = !this.#isOpen; + this.#render(); + } + + #onEventClick(issueEvent: PerfIssueEvent): void { + if (this.#onEventSelected) { + this.#onEventSelected(issueEvent.event); + } + } + + #onEventKeyDown(event: KeyboardEvent, issueEvent: PerfIssueEvent): void { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.#onEventClick(issueEvent); + } + } + + #renderDropdownIcon(isOpen: boolean): Lit.TemplateResult { + return html` + + `; + } + + #renderSeverityIcon(severity: PerfIssueSeverity): Lit.TemplateResult { + const iconData = { + error: {iconName: 'issue-cross-filled', color: 'var(--icon-error)'}, + warning: {iconName: 'issue-exclamation-filled', color: 'var(--icon-warning)'}, + info: {iconName: 'issue-text-filled', color: 'var(--icon-info)'}, + } as const; + const {iconName, color} = iconData[severity]; + return html` + + `; + } + + #render(): void { + const issue = this.#issue; + if (!issue) { + Lit.render(Lit.nothing, this.#shadow, {host: this}); + return; + } + + const formatter = new Intl.NumberFormat(undefined, { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + style: 'decimal', + }); + const contents = html` + +
+ this.#toggleOpen(e)} class="issue-summary"> + ${this.#renderDropdownIcon(this.#isOpen)} + ${this.#renderSeverityIcon(issue.severity)} + ${issue.count} +
+
${issue.name}
+ ${issue.description ? html`
${issue.description}
` : ''} + ${issue.learnMoreUrl ? html` +
+ ${UI.XLink.XLink.create(issue.learnMoreUrl as Platform.DevToolsPath.UrlString, 'Learn more')} +
+ ` : ''} +
+
+
+
+ ${issue.events.map((issueEvent, i) => { + return html` +
this.#onEventClick(issueEvent)} + @keydown=${(event: KeyboardEvent) => this.#onEventKeyDown(event, issueEvent)}> +
${issueEvent.event.name}
+
${formatter.format(issueEvent.timestampMs)} ms
+
`; + })} +
+
+
+ `; + Lit.render(contents, this.#shadow, {host: this}); + } +} + +customElements.define('devtools-performance-sidebar-perf-issue-item', SidebarRNPerfIssueItem); + +declare global { + interface HTMLElementTagNameMap { + 'devtools-performance-sidebar-perf-issue-item': SidebarRNPerfIssueItem; + } +} diff --git a/front_end/panels/timeline/components/SidebarRNPerfSignalsTab.ts b/front_end/panels/timeline/components/SidebarRNPerfSignalsTab.ts new file mode 100644 index 00000000000..fa39864d7c7 --- /dev/null +++ b/front_end/panels/timeline/components/SidebarRNPerfSignalsTab.ts @@ -0,0 +1,171 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// Copyright 2024 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 '../../../ui/components/buttons/buttons.js'; +import '../../../ui/components/icon_button/icon_button.js'; +import './SidebarRNPerfIssueItem.js'; + +import * as i18n from '../../../core/i18n/i18n.js'; +import * as Trace from '../../../models/trace/trace.js'; +import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js'; +import * as Lit from '../../../ui/lit/lit.js'; + +import type {AggregatedPerfIssue, PerfIssueEvent, PerfIssueSeverity, RNPerfIssueDetail} from './RNPerfIssueTypes.js'; +import styles from './sidebarRNPerfIssuesTab.css.js'; + +const {html} = Lit; + +const DEFAULT_ISSUE_SEVERITY = 'info'; +const SORT_ORDER: Record = { + error: 3, + warning: 2, + info: 1, +}; + +const UIStrings = { + /** @description Title for empty state */ + emptyStateTitle: 'Performance Signals (Experimental)', + /** @description Description for empty state */ + emptyStateDetail: 'No issues found', +} as const; + +const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/SidebarRNPerfSignalsTab.ts', UIStrings); +const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); + +export class SidebarRNPerfSignalsTab extends HTMLElement { + readonly #boundRender = this.#render.bind(this); + readonly #shadow = this.attachShadow({mode: 'open'}); + + #parsedTrace: Trace.Handlers.Types.ParsedTrace|null = null; + #perfIssues: AggregatedPerfIssue[] = []; + + #selectTimelineEvent?: (event: Trace.Types.Events.Event) => void; + + set parsedTrace(data: Trace.Handlers.Types.ParsedTrace|null) { + if (data === this.#parsedTrace) { + return; + } + this.#parsedTrace = data; + this.#updatePerfIssues(); + void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender); + } + + setSelectTimelineEventCallback(callback: (event: Trace.Types.Events.Event) => void): void { + this.#selectTimelineEvent = callback; + } + + hasIssues(): boolean { + return this.#perfIssues.length > 0; + } + + #updatePerfIssues(): void { + this.#perfIssues = []; + if (!this.#parsedTrace) { + return; + } + + const traceStartMs = Trace.Helpers.Timing.microToMilli(this.#parsedTrace.Meta.traceBounds.min); + const eventsByIssueName = new Map(); + + // Find extension track entries (rendered in the flame chart) in the parsed + // trace that contain `detail.devtools.performanceIssue` + for (const extensionTrack of this.#parsedTrace.ExtensionTraceData.extensionTrackData) { + for (const entries of Object.values(extensionTrack.entriesByTrack)) { + for (const extensionEntry of entries) { + if (!Trace.Types.Extensions.isSyntheticExtensionEntry(extensionEntry)) { + continue; + } + + const rawSourceEvent = extensionEntry.rawSourceEvent; + try { + const sourceEventWithDetail = Trace.Types.Events.isSyntheticUserTiming(rawSourceEvent) ? + rawSourceEvent.args.data.beginEvent : + rawSourceEvent; + const detailString = 'detail' in sourceEventWithDetail.args ? sourceEventWithDetail.args.detail : undefined; + if (!detailString) { + continue; + } + const detail = JSON.parse(detailString); + const perfIssueDetail = detail?.devtools?.performanceIssue as RNPerfIssueDetail; + if (!perfIssueDetail) { + continue; + } + + const issueData = eventsByIssueName.get(perfIssueDetail.name) ?? { + metadata: perfIssueDetail, + events: [], + }; + issueData.events.push({ + event: extensionEntry, // Use the extension entry, not the user timing pair + timestampMs: Trace.Helpers.Timing.microToMilli(extensionEntry.ts) - traceStartMs, + }); + eventsByIssueName.set(perfIssueDetail.name, issueData); + } catch { + continue; + } + } + } + } + + for (const [name, {events, metadata}] of eventsByIssueName) { + this.#perfIssues.push({ + name, + description: metadata.description, + severity: metadata.severity ?? DEFAULT_ISSUE_SEVERITY, + learnMoreUrl: metadata.learnMoreUrl, + events, + count: events.length, + }); + } + + this.#perfIssues.sort((a, b) => { + const severityDiff = SORT_ORDER[b.severity] - SORT_ORDER[a.severity]; + if (severityDiff !== 0) { + return severityDiff; + } + return a.name.localeCompare(b.name); + }); + } + + #renderEmptyState(): Lit.TemplateResult { + return html` +
+
${i18nString(UIStrings.emptyStateTitle)}
+
${i18nString(UIStrings.emptyStateDetail)}
+
+ `; + } + + #render(): void { + if (!this.#parsedTrace) { + Lit.render(Lit.nothing, this.#shadow, {host: this}); + return; + } + + const contents = html` + +
+ ${this.#perfIssues.length ? + this.#perfIssues.map(issue => html` + + `) : + this.#renderEmptyState() + } +
+ `; + Lit.render(contents, this.#shadow, {host: this}); + } +} + +customElements.define('devtools-performance-sidebar-perf-signals', SidebarRNPerfSignalsTab); + +declare global { + interface HTMLElementTagNameMap { + 'devtools-performance-sidebar-perf-signals': SidebarRNPerfSignalsTab; + } +} diff --git a/front_end/panels/timeline/components/sidebarRNPerfIssueItem.css b/front_end/panels/timeline/components/sidebarRNPerfIssueItem.css new file mode 100644 index 00000000000..63a9733d70a --- /dev/null +++ b/front_end/panels/timeline/components/sidebarRNPerfIssueItem.css @@ -0,0 +1,142 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * Copyright 2024 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. + */ + +:host { + display: block; +} + +details { + border-bottom: 1px solid var(--sys-color-divider); +} + +.issue-summary { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + list-style: none; + background-color: var(--color-background); + transition: background-color 0.2s ease; +} + +.issue-summary:hover { + background-color: var(--sys-color-state-hover-on-subtle); +} + +.issue-summary::-webkit-details-marker { + display: none; +} + +.dropdown-icon { + transition: transform 0.2s ease; + flex-shrink: 0; +} + +.dropdown-icon.open { + transform: rotate(90deg); +} + +.issue-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.issue-name { + font-weight: 500; +} + +.event-count-pill { + background-color: var(--sys-color-neutral-container); + color: var(--sys-color-on-surface-subtle); + border-radius: 10px; + padding: 2px 6px; + font-size: 11px; + font-weight: 500; + min-width: 16px; + text-align: center; + line-height: 1.2; + display: inline-block; + flex-shrink: 0; + margin-top: 1px; +} + +.issue-summary devtools-icon { + margin-top: 1px; +} + +.issue-description { + font-size: 12px; + color: var(--color-text-secondary); + line-height: 1.3; +} + +.issue-learn-more { + font-size: 12px; + color: var(--text-link); + text-decoration: underline; +} + +.issue-content { + padding: 0 12px 8px; + background-color: var(--color-background); +} + +.event-list { + border-left: 2px solid var(--sys-color-divider); + margin-left: 30px; +} + +.event-list-error { + border-left-color: var(--icon-error); +} + +.event-list-warning { + border-left-color: var(--icon-warning); +} + +.event-list-info { + border-left-color: var(--icon-info); +} + +.event-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px 12px; + cursor: pointer; + border-bottom: 1px solid var(--sys-color-divider); +} + +.event-item:focus { + outline: 2px solid var(--sys-color-state-focus-ring); + outline-offset: -2px; +} + +.event-item:last-child { + border-bottom: none; +} + +.event-name { + font-size: 12px; + color: var(--text-link); + flex: 1; + font-weight: 400; + text-decoration: underline; + cursor: pointer; +} + +.event-timestamp { + font-size: 11px; + color: var(--color-text-secondary); + margin-left: 8px; + font-weight: 500; + white-space: nowrap; +} + diff --git a/front_end/panels/timeline/components/sidebarRNPerfIssuesTab.css b/front_end/panels/timeline/components/sidebarRNPerfIssuesTab.css new file mode 100644 index 00000000000..7d0cdd5d736 --- /dev/null +++ b/front_end/panels/timeline/components/sidebarRNPerfIssuesTab.css @@ -0,0 +1,37 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * Copyright 2024 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. + */ + +.perf-issues-wrapper { + flex: 1; + overflow-y: auto; +} + +.empty-state-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 16px; +} + +.empty-state-title { + font-size: 14px; + font-weight: 500; + color: var(--color-text-primary); + margin-bottom: 8px; + text-align: center; +} + +.empty-state-detail { + font-size: 14px; + color: var(--color-text-secondary); + line-height: 1.4; + text-align: center; +} + + diff --git a/scripts/eslint_rules/lib/check-license-header.js b/scripts/eslint_rules/lib/check-license-header.js index 6ccaffa191d..3d6dff28525 100644 --- a/scripts/eslint_rules/lib/check-license-header.js +++ b/scripts/eslint_rules/lib/check-license-header.js @@ -86,6 +86,9 @@ const META_CODE_PATHS = [ 'panels/react_devtools', 'panels/rn_welcome', 'panels/timeline/ReactNativeTimelineLandingPage.ts', + 'panels/timeline/components/RNPerfIssueTypes.ts', + 'panels/timeline/components/SidebarRNPerfIssueItem.ts', + 'panels/timeline/components/SidebarRNPerfSignalsTab.ts', ]; const OTHER_LICENSE_HEADERS = [